From 400daa4faf1b6595a9d9a17eb5c35426caeeb2d9 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 26 Jul 2026 12:51:29 -0400 Subject: [PATCH 01/32] Allow owners and admins to delete workspaces from settings. Expose a danger zone with name confirmation, sync Stripe quantity on SaaS, and skip billing constraints in self-hosted mode. Co-authored-by: Cursor --- .../Controllers/App/WorkspaceController.php | 3 + app/Policies/WorkspacePolicy.php | 6 +- lang/ar/settings.php | 12 +++ lang/de/settings.php | 12 +++ lang/el/settings.php | 12 +++ lang/en/settings.php | 12 +++ lang/es/settings.php | 12 +++ lang/fr/settings.php | 12 +++ lang/it/settings.php | 12 +++ lang/ja/settings.php | 12 +++ lang/ko/settings.php | 12 +++ lang/nl/settings.php | 12 +++ lang/pl/settings.php | 12 +++ lang/pt-BR/settings.php | 12 +++ lang/ru/settings.php | 12 +++ lang/tr/settings.php | 12 +++ lang/zh/settings.php | 12 +++ .../js/components/settings/WorkspaceTab.vue | 86 ++++++++++++++++++- resources/js/composables/useWorkspaceRole.ts | 1 + .../js/pages/settings/workspace/Workspace.vue | 8 +- tests/Feature/WorkspaceControllerTest.php | 80 +++++++++++++++++ tests/Unit/Policies/WorkspacePolicyTest.php | 20 +++-- 22 files changed, 372 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/App/WorkspaceController.php b/app/Http/Controllers/App/WorkspaceController.php index 5b2326f2d..f43d749a5 100644 --- a/app/Http/Controllers/App/WorkspaceController.php +++ b/app/Http/Controllers/App/WorkspaceController.php @@ -154,6 +154,9 @@ public function settings(Request $request): Response|RedirectResponse return Inertia::render('settings/workspace/Workspace', [ 'workspace' => $workspace, + 'canDelete' => $user->can('delete', $workspace), + 'isOnlyWorkspace' => ! config('trypost.self_hosted') + && $workspace->account->workspaces()->count() <= 1, ]); } diff --git a/app/Policies/WorkspacePolicy.php b/app/Policies/WorkspacePolicy.php index ad4f40b34..028d37fdc 100644 --- a/app/Policies/WorkspacePolicy.php +++ b/app/Policies/WorkspacePolicy.php @@ -32,17 +32,17 @@ public function update(User $user, Workspace $workspace): bool public function delete(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace); + return $this->isOwnerOrWorkspaceAdmin($user, $workspace); } public function restore(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace); + return $this->isOwnerOrWorkspaceAdmin($user, $workspace); } public function forceDelete(User $user, Workspace $workspace): bool { - return $this->isOwner($user, $workspace); + return $this->isOwnerOrWorkspaceAdmin($user, $workspace); } public function manageTeam(User $user, Workspace $workspace): bool diff --git a/lang/ar/settings.php b/lang/ar/settings.php index fc98446e5..bd302eadb 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -140,6 +140,18 @@ 'name' => 'الاسم', 'name_placeholder' => 'مساحة عملي', 'save' => 'حفظ', + 'danger_title' => 'منطقة الخطر', + 'danger_description' => 'إجراءات لا يمكن التراجع عنها.', + 'delete_title' => 'حذف مساحة العمل هذه', + 'delete_description' => 'يحذف مساحة العمل هذه ومنشوراتها والحسابات المتصلة والوسائط نهائيًا. ستنخفض كمية اشتراكك بمقدار واحد.', + 'delete_description_self_hosted' => 'يحذف مساحة العمل هذه ومنشوراتها والحسابات المتصلة والوسائط نهائيًا.', + 'delete_only_description' => 'لا يمكنك حذف مساحة العمل الوحيدة لديك. ألغِ اشتراكك في إعدادات الفوترة لإغلاق حسابك.', + 'delete_go_to_billing' => 'الانتقال إلى الفوترة', + 'delete_action' => 'حذف مساحة العمل', + 'delete_cancel' => 'إلغاء', + 'delete_confirm_title' => 'حذف مساحة العمل؟', + 'delete_confirm_description' => 'سيؤدي هذا إلى حذف مساحة العمل وجميع بياناتها نهائيًا. سيتم تحديث الفوترة لتعكس مساحة عمل أقل.', + 'delete_confirm_description_self_hosted' => 'سيؤدي هذا إلى حذف مساحة العمل وجميع بياناتها نهائيًا.', ], 'brand' => [ diff --git a/lang/de/settings.php b/lang/de/settings.php index 2d19b929c..63cd154b8 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -142,6 +142,18 @@ 'name' => 'Name', 'name_placeholder' => 'Mein Workspace', 'save' => 'Speichern', + 'danger_title' => 'Gefahrenzone', + 'danger_description' => 'Unwiderrufliche Aktionen.', + 'delete_title' => 'Diesen Workspace löschen', + 'delete_description' => 'Löscht diesen Workspace dauerhaft inklusive Beiträge, verbundener Konten und Medien. Die Menge deines Abonnements wird um eins verringert.', + 'delete_description_self_hosted' => 'Löscht diesen Workspace dauerhaft inklusive Beiträge, verbundener Konten und Medien.', + 'delete_only_description' => 'Du kannst deinen einzigen Workspace nicht löschen. Kündige dein Abonnement in den Abrechnungseinstellungen, um dein Konto zu schließen.', + 'delete_go_to_billing' => 'Zur Abrechnung', + 'delete_action' => 'Workspace löschen', + 'delete_cancel' => 'Abbrechen', + 'delete_confirm_title' => 'Workspace löschen?', + 'delete_confirm_description' => 'Dadurch wird der Workspace und alle seine Daten dauerhaft gelöscht. Deine Abrechnung wird um einen Workspace reduziert.', + 'delete_confirm_description_self_hosted' => 'Dadurch wird der Workspace und alle seine Daten dauerhaft gelöscht.', ], 'brand' => [ diff --git a/lang/el/settings.php b/lang/el/settings.php index 0a2a638ed..eb8313172 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -140,6 +140,18 @@ 'name' => 'Όνομα', 'name_placeholder' => 'Το workspace μου', 'save' => 'Αποθήκευση', + 'danger_title' => 'Ζώνη κινδύνου', + 'danger_description' => 'Μη αναστρέψιμες ενέργειες.', + 'delete_title' => 'Διαγραφή αυτού του workspace', + 'delete_description' => 'Διαγράφει οριστικά αυτό το workspace, τις αναρτήσεις, τους συνδεδεμένους λογαριασμούς και τα μέσα. Η ποσότητα της συνδρομής σας θα μειωθεί κατά ένα.', + 'delete_description_self_hosted' => 'Διαγράφει οριστικά αυτό το workspace, τις αναρτήσεις, τους συνδεδεμένους λογαριασμούς και τα μέσα.', + 'delete_only_description' => 'Δεν μπορείτε να διαγράψετε το μοναδικό σας workspace. Ακυρώστε τη συνδρομή σας στις ρυθμίσεις χρέωσης για να κλείσετε τον λογαριασμό σας.', + 'delete_go_to_billing' => 'Μετάβαση στη χρέωση', + 'delete_action' => 'Διαγραφή workspace', + 'delete_cancel' => 'Ακύρωση', + 'delete_confirm_title' => 'Διαγραφή workspace;', + 'delete_confirm_description' => 'Αυτό διαγράφει οριστικά το workspace και όλα τα δεδομένα του. Η χρέωσή σας θα ενημερωθεί ώστε να αντικατοπτρίζει ένα λιγότερο workspace.', + 'delete_confirm_description_self_hosted' => 'Αυτό διαγράφει οριστικά το workspace και όλα τα δεδομένα του.', ], 'brand' => [ diff --git a/lang/en/settings.php b/lang/en/settings.php index 94858730c..5b93f5bd1 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -140,6 +140,18 @@ 'name' => 'Name', 'name_placeholder' => 'My Workspace', 'save' => 'Save', + 'danger_title' => 'Danger zone', + 'danger_description' => 'Irreversible actions.', + 'delete_title' => 'Delete this workspace', + 'delete_description' => 'Permanently deletes this workspace, its posts, connected accounts, and media. Your subscription quantity will decrease by one.', + 'delete_description_self_hosted' => 'Permanently deletes this workspace, its posts, connected accounts, and media.', + 'delete_only_description' => 'You cannot delete your only workspace. Cancel your subscription in billing settings to close your account.', + 'delete_go_to_billing' => 'Go to billing', + 'delete_action' => 'Delete workspace', + 'delete_cancel' => 'Cancel', + 'delete_confirm_title' => 'Delete workspace?', + 'delete_confirm_description' => 'This permanently deletes the workspace and all of its data. Your billing will update to reflect one fewer workspace.', + 'delete_confirm_description_self_hosted' => 'This permanently deletes the workspace and all of its data.', ], 'brand' => [ diff --git a/lang/es/settings.php b/lang/es/settings.php index 34053b17c..df33c2ca3 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -140,6 +140,18 @@ 'name' => 'Nombre', 'name_placeholder' => 'Mi Workspace', 'save' => 'Guardar', + 'danger_title' => 'Zona de peligro', + 'danger_description' => 'Acciones irreversibles.', + 'delete_title' => 'Eliminar este workspace', + 'delete_description' => 'Elimina permanentemente este workspace, sus publicaciones, cuentas conectadas y medios. La cantidad de tu suscripción disminuirá en uno.', + 'delete_description_self_hosted' => 'Elimina permanentemente este workspace, sus publicaciones, cuentas conectadas y medios.', + 'delete_only_description' => 'No puedes eliminar tu único workspace. Cancela tu suscripción en la configuración de facturación para cerrar tu cuenta.', + 'delete_go_to_billing' => 'Ir a facturación', + 'delete_action' => 'Eliminar workspace', + 'delete_cancel' => 'Cancelar', + 'delete_confirm_title' => '¿Eliminar workspace?', + 'delete_confirm_description' => 'Esto elimina permanentemente el workspace y todos sus datos. Tu facturación se actualizará para reflejar un workspace menos.', + 'delete_confirm_description_self_hosted' => 'Esto elimina permanentemente el workspace y todos sus datos.', ], 'brand' => [ diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 83e092baf..478170cf0 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -140,6 +140,18 @@ 'name' => 'Nom', 'name_placeholder' => 'Mon espace de travail', 'save' => 'Enregistrer', + 'danger_title' => 'Zone de danger', + 'danger_description' => 'Actions irréversibles.', + 'delete_title' => 'Supprimer cet espace de travail', + 'delete_description' => 'Supprime définitivement cet espace de travail, ses publications, comptes connectés et médias. La quantité de votre abonnement diminuera d’un.', + 'delete_description_self_hosted' => 'Supprime définitivement cet espace de travail, ses publications, comptes connectés et médias.', + 'delete_only_description' => 'Vous ne pouvez pas supprimer votre seul espace de travail. Annulez votre abonnement dans les paramètres de facturation pour fermer votre compte.', + 'delete_go_to_billing' => 'Aller à la facturation', + 'delete_action' => 'Supprimer l’espace de travail', + 'delete_cancel' => 'Annuler', + 'delete_confirm_title' => 'Supprimer l’espace de travail ?', + 'delete_confirm_description' => 'Cela supprime définitivement l’espace de travail et toutes ses données. Votre facturation sera mise à jour pour refléter un espace de travail en moins.', + 'delete_confirm_description_self_hosted' => 'Cela supprime définitivement l’espace de travail et toutes ses données.', ], 'brand' => [ diff --git a/lang/it/settings.php b/lang/it/settings.php index e34c3e9d3..7a22b2c89 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -140,6 +140,18 @@ 'name' => 'Nome', 'name_placeholder' => 'Il mio workspace', 'save' => 'Salva', + 'danger_title' => 'Zona pericolosa', + 'danger_description' => 'Azioni irreversibili.', + 'delete_title' => 'Elimina questo workspace', + 'delete_description' => 'Elimina definitivamente questo workspace, i suoi post, gli account collegati e i media. La quantità del tuo abbonamento diminuirà di uno.', + 'delete_description_self_hosted' => 'Elimina definitivamente questo workspace, i suoi post, gli account collegati e i media.', + 'delete_only_description' => 'Non puoi eliminare il tuo unico workspace. Annulla il tuo abbonamento nelle impostazioni di fatturazione per chiudere il tuo account.', + 'delete_go_to_billing' => 'Vai alla fatturazione', + 'delete_action' => 'Elimina workspace', + 'delete_cancel' => 'Annulla', + 'delete_confirm_title' => 'Eliminare il workspace?', + 'delete_confirm_description' => 'Questo elimina definitivamente il workspace e tutti i suoi dati. La fatturazione verrà aggiornata per riflettere un workspace in meno.', + 'delete_confirm_description_self_hosted' => 'Questo elimina definitivamente il workspace e tutti i suoi dati.', ], 'brand' => [ diff --git a/lang/ja/settings.php b/lang/ja/settings.php index 80eae74b4..d9f5cbeca 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -140,6 +140,18 @@ 'name' => '名前', 'name_placeholder' => 'マイワークスペース', 'save' => '保存', + 'danger_title' => '危険な操作', + 'danger_description' => '元に戻せない操作です。', + 'delete_title' => 'このワークスペースを削除', + 'delete_description' => 'このワークスペースと投稿、連携アカウント、メディアを完全に削除します。サブスクリプションの数量は1つ減ります。', + 'delete_description_self_hosted' => 'このワークスペースと投稿、連携アカウント、メディアを完全に削除します。', + 'delete_only_description' => '唯一のワークスペースは削除できません。アカウントを閉じるには、請求設定でサブスクリプションを解約してください。', + 'delete_go_to_billing' => '請求へ移動', + 'delete_action' => 'ワークスペースを削除', + 'delete_cancel' => 'キャンセル', + 'delete_confirm_title' => 'ワークスペースを削除しますか?', + 'delete_confirm_description' => 'ワークスペースとそのすべてのデータが完全に削除されます。請求はワークスペースが1つ減った内容に更新されます。', + 'delete_confirm_description_self_hosted' => 'ワークスペースとそのすべてのデータが完全に削除されます。', ], 'brand' => [ diff --git a/lang/ko/settings.php b/lang/ko/settings.php index feb6bf182..714663a95 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -140,6 +140,18 @@ 'name' => '이름', 'name_placeholder' => '내 워크스페이스', 'save' => '저장', + 'danger_title' => '위험 구역', + 'danger_description' => '되돌릴 수 없는 작업입니다.', + 'delete_title' => '이 워크스페이스 삭제', + 'delete_description' => '이 워크스페이스와 게시물, 연결된 계정, 미디어를 영구 삭제합니다. 구독 수량이 하나 줄어듭니다.', + 'delete_description_self_hosted' => '이 워크스페이스와 게시물, 연결된 계정, 미디어를 영구 삭제합니다.', + 'delete_only_description' => '유일한 워크스페이스는 삭제할 수 없습니다. 계정을 닫으려면 결제 설정에서 구독을 취소하세요.', + 'delete_go_to_billing' => '결제로 이동', + 'delete_action' => '워크스페이스 삭제', + 'delete_cancel' => '취소', + 'delete_confirm_title' => '워크스페이스를 삭제할까요?', + 'delete_confirm_description' => '워크스페이스와 모든 데이터가 영구 삭제됩니다. 결제는 워크스페이스가 하나 줄어든 상태로 업데이트됩니다.', + 'delete_confirm_description_self_hosted' => '워크스페이스와 모든 데이터가 영구 삭제됩니다.', ], 'brand' => [ diff --git a/lang/nl/settings.php b/lang/nl/settings.php index 6a5b42a8f..dd9c16a42 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -140,6 +140,18 @@ 'name' => 'Naam', 'name_placeholder' => 'Mijn workspace', 'save' => 'Opslaan', + 'danger_title' => 'Gevarenzone', + 'danger_description' => 'Onomkeerbare acties.', + 'delete_title' => 'Deze workspace verwijderen', + 'delete_description' => 'Verwijdert deze workspace permanent, inclusief posts, gekoppelde accounts en media. De hoeveelheid van je abonnement daalt met één.', + 'delete_description_self_hosted' => 'Verwijdert deze workspace permanent, inclusief posts, gekoppelde accounts en media.', + 'delete_only_description' => 'Je kunt je enige workspace niet verwijderen. Zeg je abonnement op in de facturatie-instellingen om je account te sluiten.', + 'delete_go_to_billing' => 'Naar facturatie', + 'delete_action' => 'Workspace verwijderen', + 'delete_cancel' => 'Annuleren', + 'delete_confirm_title' => 'Workspace verwijderen?', + 'delete_confirm_description' => 'Dit verwijdert de workspace en al zijn gegevens permanent. Je facturatie wordt bijgewerkt met één workspace minder.', + 'delete_confirm_description_self_hosted' => 'Dit verwijdert de workspace en al zijn gegevens permanent.', ], 'brand' => [ diff --git a/lang/pl/settings.php b/lang/pl/settings.php index bfd2db6c4..365da11dc 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -140,6 +140,18 @@ 'name' => 'Nazwa', 'name_placeholder' => 'Moja przestrzeń robocza', 'save' => 'Zapisz', + 'danger_title' => 'Strefa zagrożenia', + 'danger_description' => 'Nieodwracalne działania.', + 'delete_title' => 'Usuń ten workspace', + 'delete_description' => 'Trwale usuwa ten workspace, jego posty, połączone konta i media. Ilość w subskrypcji zmniejszy się o jeden.', + 'delete_description_self_hosted' => 'Trwale usuwa ten workspace, jego posty, połączone konta i media.', + 'delete_only_description' => 'Nie możesz usunąć swojego jedynego workspace. Anuluj subskrypcję w ustawieniach rozliczeń, aby zamknąć konto.', + 'delete_go_to_billing' => 'Przejdź do rozliczeń', + 'delete_action' => 'Usuń workspace', + 'delete_cancel' => 'Anuluj', + 'delete_confirm_title' => 'Usunąć workspace?', + 'delete_confirm_description' => 'To trwale usuwa workspace i wszystkie jego dane. Rozliczenia zostaną zaktualizowane, aby odzwierciedlić o jeden workspace mniej.', + 'delete_confirm_description_self_hosted' => 'To trwale usuwa workspace i wszystkie jego dane.', ], 'brand' => [ diff --git a/lang/pt-BR/settings.php b/lang/pt-BR/settings.php index 7ad405879..758f35b28 100644 --- a/lang/pt-BR/settings.php +++ b/lang/pt-BR/settings.php @@ -140,6 +140,18 @@ 'name' => 'Nome', 'name_placeholder' => 'Meu Workspace', 'save' => 'Salvar', + 'danger_title' => 'Zona de perigo', + 'danger_description' => 'Ações irreversíveis.', + 'delete_title' => 'Excluir este workspace', + 'delete_description' => 'Exclui permanentemente este workspace, seus posts, contas conectadas e mídia. A quantidade da sua assinatura será reduzida em um.', + 'delete_description_self_hosted' => 'Exclui permanentemente este workspace, seus posts, contas conectadas e mídia.', + 'delete_only_description' => 'Você não pode excluir seu único workspace. Cancele sua assinatura nas configurações de cobrança para encerrar sua conta.', + 'delete_go_to_billing' => 'Ir para cobrança', + 'delete_action' => 'Excluir workspace', + 'delete_cancel' => 'Cancelar', + 'delete_confirm_title' => 'Excluir workspace?', + 'delete_confirm_description' => 'Isso exclui permanentemente o workspace e todos os seus dados. Sua cobrança será atualizada para refletir um workspace a menos.', + 'delete_confirm_description_self_hosted' => 'Isso exclui permanentemente o workspace e todos os seus dados.', ], 'brand' => [ diff --git a/lang/ru/settings.php b/lang/ru/settings.php index adfeeefdd..235ae3355 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -140,6 +140,18 @@ 'name' => 'Название', 'name_placeholder' => 'Моё рабочее пространство', 'save' => 'Сохранить', + 'danger_title' => 'Опасная зона', + 'danger_description' => 'Необратимые действия.', + 'delete_title' => 'Удалить этот workspace', + 'delete_description' => 'Безвозвратно удаляет этот workspace, его посты, подключённые аккаунты и медиа. Количество в подписке уменьшится на один.', + 'delete_description_self_hosted' => 'Безвозвратно удаляет этот workspace, его посты, подключённые аккаунты и медиа.', + 'delete_only_description' => 'Вы не можете удалить свой единственный workspace. Отмените подписку в настройках оплаты, чтобы закрыть аккаунт.', + 'delete_go_to_billing' => 'Перейти к оплате', + 'delete_action' => 'Удалить workspace', + 'delete_cancel' => 'Отмена', + 'delete_confirm_title' => 'Удалить workspace?', + 'delete_confirm_description' => 'Это безвозвратно удалит workspace и все его данные. Оплата обновится с учётом на один workspace меньше.', + 'delete_confirm_description_self_hosted' => 'Это безвозвратно удалит workspace и все его данные.', ], 'brand' => [ diff --git a/lang/tr/settings.php b/lang/tr/settings.php index 148bb834b..ceef55a75 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -142,6 +142,18 @@ 'name' => 'Ad', 'name_placeholder' => 'Çalışma Alanım', 'save' => 'Kaydet', + 'danger_title' => 'Tehlikeli bölge', + 'danger_description' => 'Geri alınamaz işlemler.', + 'delete_title' => 'Bu workspace’i sil', + 'delete_description' => 'Bu workspace’i, gönderilerini, bağlı hesaplarını ve medyasını kalıcı olarak siler. Abonelik miktarınız bir azalır.', + 'delete_description_self_hosted' => 'Bu workspace’i, gönderilerini, bağlı hesaplarını ve medyasını kalıcı olarak siler.', + 'delete_only_description' => 'Tek workspace’inizi silemezsiniz. Hesabınızı kapatmak için faturalandırma ayarlarından aboneliğinizi iptal edin.', + 'delete_go_to_billing' => 'Faturalandırmaya git', + 'delete_action' => 'Workspace’i sil', + 'delete_cancel' => 'İptal', + 'delete_confirm_title' => 'Workspace silinsin mi?', + 'delete_confirm_description' => 'Bu, workspace’i ve tüm verilerini kalıcı olarak siler. Faturalandırma bir workspace eksilecek şekilde güncellenir.', + 'delete_confirm_description_self_hosted' => 'Bu, workspace’i ve tüm verilerini kalıcı olarak siler.', ], 'brand' => [ diff --git a/lang/zh/settings.php b/lang/zh/settings.php index eff7e82ca..6e27981ed 100644 --- a/lang/zh/settings.php +++ b/lang/zh/settings.php @@ -140,6 +140,18 @@ 'name' => '名称', 'name_placeholder' => '我的工作区', 'save' => '保存', + 'danger_title' => '危险区域', + 'danger_description' => '不可撤销的操作。', + 'delete_title' => '删除此工作区', + 'delete_description' => '永久删除此工作区及其帖子、已连接账户和媒体。订阅数量将减少一个。', + 'delete_description_self_hosted' => '永久删除此工作区及其帖子、已连接账户和媒体。', + 'delete_only_description' => '无法删除唯一的工作区。请在账单设置中取消订阅以关闭账户。', + 'delete_go_to_billing' => '前往账单', + 'delete_action' => '删除工作区', + 'delete_cancel' => '取消', + 'delete_confirm_title' => '删除工作区?', + 'delete_confirm_description' => '这将永久删除工作区及其所有数据。账单将更新为少一个工作区。', + 'delete_confirm_description_self_hosted' => '这将永久删除工作区及其所有数据。', ], 'brand' => [ diff --git a/resources/js/components/settings/WorkspaceTab.vue b/resources/js/components/settings/WorkspaceTab.vue index cad7a69c7..d6f3ac5ba 100644 --- a/resources/js/components/settings/WorkspaceTab.vue +++ b/resources/js/components/settings/WorkspaceTab.vue @@ -1,7 +1,10 @@ diff --git a/resources/js/composables/useWorkspaceRole.ts b/resources/js/composables/useWorkspaceRole.ts index 52ae869ed..d7eabf7c9 100644 --- a/resources/js/composables/useWorkspaceRole.ts +++ b/resources/js/composables/useWorkspaceRole.ts @@ -33,6 +33,7 @@ export const useWorkspaceRole = () => { canManageAccounts: isAdminOrAbove, canManageTeam: isAdminOrAbove, canManageWorkspace: isAdminOrAbove, + canDeleteWorkspace: isAdminOrAbove, canManageBilling: isOwner, canCreateWorkspace: isOwner, }; diff --git a/resources/js/pages/settings/workspace/Workspace.vue b/resources/js/pages/settings/workspace/Workspace.vue index f525273df..7c244ca0a 100644 --- a/resources/js/pages/settings/workspace/Workspace.vue +++ b/resources/js/pages/settings/workspace/Workspace.vue @@ -24,6 +24,8 @@ interface Workspace { defineProps<{ workspace: Workspace; + canDelete: boolean; + isOnlyWorkspace: boolean; }>(); const tabs = computed(() => [ @@ -46,7 +48,11 @@ const tabs = computed(() => [ - + diff --git a/tests/Feature/WorkspaceControllerTest.php b/tests/Feature/WorkspaceControllerTest.php index e1354dc58..df84876ff 100644 --- a/tests/Feature/WorkspaceControllerTest.php +++ b/tests/Feature/WorkspaceControllerTest.php @@ -180,6 +180,48 @@ $response->assertInertia(fn ($page) => $page ->component('settings/workspace/Workspace', false) ->has('workspace') + ->where('canDelete', true) + ->where('isOnlyWorkspace', false) + ); +}); + +test('workspace settings marks only workspace in saas mode', function () { + config(['trypost.self_hosted' => false]); + $this->user->account->subscriptions()->create([ + 'type' => Account::SUBSCRIPTION_NAME, + 'stripe_id' => 'sub_test_'.fake()->uuid(), + 'stripe_status' => 'active', + 'stripe_price' => 'price_123', + ]); + + $response = $this->actingAs($this->user)->get(route('app.workspace.settings')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->where('canDelete', true) + ->where('isOnlyWorkspace', true) + ); +}); + +test('workspace settings does not mark only workspace when account has more than one', function () { + config(['trypost.self_hosted' => false]); + $this->user->account->subscriptions()->create([ + 'type' => Account::SUBSCRIPTION_NAME, + 'stripe_id' => 'sub_test_'.fake()->uuid(), + 'stripe_status' => 'active', + 'stripe_price' => 'price_123', + ]); + + Workspace::factory()->create([ + 'account_id' => $this->user->account_id, + 'user_id' => $this->user->id, + ]); + + $response = $this->actingAs($this->user)->get(route('app.workspace.settings')); + + $response->assertOk(); + $response->assertInertia(fn ($page) => $page + ->where('isOnlyWorkspace', false) ); }); @@ -508,6 +550,44 @@ $response->assertForbidden(); }); +test('destroy workspace allows workspace admin', function () { + Workspace::factory()->create([ + 'account_id' => $this->user->account_id, + 'user_id' => $this->user->id, + ]); + + $admin = User::factory()->create([ + 'account_id' => $this->user->account_id, + 'current_workspace_id' => $this->workspace->id, + ]); + $this->workspace->members()->attach($admin->id, ['role' => Role::Admin->value]); + + $workspaceId = $this->workspace->id; + + $response = $this->actingAs($admin)->delete(route('app.workspaces.destroy', $this->workspace)); + + $response->assertRedirect(route('app.workspaces.index')); + expect(Workspace::find($workspaceId))->toBeNull(); +}); + +test('destroy workspace returns 403 for workspace member', function () { + Workspace::factory()->create([ + 'account_id' => $this->user->account_id, + 'user_id' => $this->user->id, + ]); + + $member = User::factory()->create([ + 'account_id' => $this->user->account_id, + 'current_workspace_id' => $this->workspace->id, + ]); + $this->workspace->members()->attach($member->id, ['role' => Role::Member->value]); + + $response = $this->actingAs($member)->delete(route('app.workspaces.destroy', $this->workspace)); + + $response->assertForbidden(); + expect(Workspace::find($this->workspace->id))->not->toBeNull(); +}); + // Autofill brand tests test('autofillBrand returns metadata without persisting anything', function () { $account = Account::factory()->create(); diff --git a/tests/Unit/Policies/WorkspacePolicyTest.php b/tests/Unit/Policies/WorkspacePolicyTest.php index 7bc3bd451..538cd0b27 100644 --- a/tests/Unit/Policies/WorkspacePolicyTest.php +++ b/tests/Unit/Policies/WorkspacePolicyTest.php @@ -133,7 +133,7 @@ expect($this->policy->update($member, $workspace))->toBeFalse(); }); -test('only account owner can delete workspace', function () { +test('account owner and workspace admin can delete workspace', function () { $account = Account::factory()->create(); $owner = User::factory()->create([ 'account_id' => $account->id, @@ -142,16 +142,22 @@ $admin = User::factory()->create([ 'account_id' => $account->id, ]); + $member = User::factory()->create([ + 'account_id' => $account->id, + ]); $workspace = Workspace::factory()->create([ 'account_id' => $account->id, 'user_id' => $owner->id, ]); + $workspace->members()->attach($admin->id, ['role' => Role::Admin->value]); + $workspace->members()->attach($member->id, ['role' => Role::Member->value]); expect($this->policy->delete($owner, $workspace))->toBeTrue(); - expect($this->policy->delete($admin, $workspace))->toBeFalse(); + expect($this->policy->delete($admin, $workspace))->toBeTrue(); + expect($this->policy->delete($member, $workspace))->toBeFalse(); }); -test('only account owner can restore workspace', function () { +test('account owner and workspace admin can restore workspace', function () { $account = Account::factory()->create(); $owner = User::factory()->create([ 'account_id' => $account->id, @@ -164,12 +170,13 @@ 'account_id' => $account->id, 'user_id' => $owner->id, ]); + $workspace->members()->attach($admin->id, ['role' => Role::Admin->value]); expect($this->policy->restore($owner, $workspace))->toBeTrue(); - expect($this->policy->restore($admin, $workspace))->toBeFalse(); + expect($this->policy->restore($admin, $workspace))->toBeTrue(); }); -test('only account owner can force delete workspace', function () { +test('account owner and workspace admin can force delete workspace', function () { $account = Account::factory()->create(); $owner = User::factory()->create([ 'account_id' => $account->id, @@ -182,9 +189,10 @@ 'account_id' => $account->id, 'user_id' => $owner->id, ]); + $workspace->members()->attach($admin->id, ['role' => Role::Admin->value]); expect($this->policy->forceDelete($owner, $workspace))->toBeTrue(); - expect($this->policy->forceDelete($admin, $workspace))->toBeFalse(); + expect($this->policy->forceDelete($admin, $workspace))->toBeTrue(); }); test('account owner and admin can manage team', function () { From 642d7d84718c9f6239b336eaa4675f50e7d3d67e Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 26 Jul 2026 12:53:14 -0400 Subject: [PATCH 02/32] Drop redundant canDelete prop from workspace settings. The settings page is already gated by update (owner/admin), which matches delete. Co-authored-by: Cursor --- app/Http/Controllers/App/WorkspaceController.php | 1 - resources/js/components/settings/WorkspaceTab.vue | 5 ++--- resources/js/composables/useWorkspaceRole.ts | 1 - resources/js/pages/settings/workspace/Workspace.vue | 2 -- tests/Feature/WorkspaceControllerTest.php | 2 -- 5 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/App/WorkspaceController.php b/app/Http/Controllers/App/WorkspaceController.php index f43d749a5..9fd70bc97 100644 --- a/app/Http/Controllers/App/WorkspaceController.php +++ b/app/Http/Controllers/App/WorkspaceController.php @@ -154,7 +154,6 @@ public function settings(Request $request): Response|RedirectResponse return Inertia::render('settings/workspace/Workspace', [ 'workspace' => $workspace, - 'canDelete' => $user->can('delete', $workspace), 'isOnlyWorkspace' => ! config('trypost.self_hosted') && $workspace->account->workspaces()->count() <= 1, ]); diff --git a/resources/js/components/settings/WorkspaceTab.vue b/resources/js/components/settings/WorkspaceTab.vue index d6f3ac5ba..ca234120f 100644 --- a/resources/js/components/settings/WorkspaceTab.vue +++ b/resources/js/components/settings/WorkspaceTab.vue @@ -26,7 +26,6 @@ interface Workspace { const props = defineProps<{ workspace: Workspace; - canDelete: boolean; isOnlyWorkspace: boolean; }>(); @@ -89,9 +88,9 @@ const openDeleteModal = () => { - + -
+
{ canManageAccounts: isAdminOrAbove, canManageTeam: isAdminOrAbove, canManageWorkspace: isAdminOrAbove, - canDeleteWorkspace: isAdminOrAbove, canManageBilling: isOwner, canCreateWorkspace: isOwner, }; diff --git a/resources/js/pages/settings/workspace/Workspace.vue b/resources/js/pages/settings/workspace/Workspace.vue index 7c244ca0a..e9ee5c788 100644 --- a/resources/js/pages/settings/workspace/Workspace.vue +++ b/resources/js/pages/settings/workspace/Workspace.vue @@ -24,7 +24,6 @@ interface Workspace { defineProps<{ workspace: Workspace; - canDelete: boolean; isOnlyWorkspace: boolean; }>(); @@ -50,7 +49,6 @@ const tabs = computed(() => [ diff --git a/tests/Feature/WorkspaceControllerTest.php b/tests/Feature/WorkspaceControllerTest.php index df84876ff..b8cd35de2 100644 --- a/tests/Feature/WorkspaceControllerTest.php +++ b/tests/Feature/WorkspaceControllerTest.php @@ -180,7 +180,6 @@ $response->assertInertia(fn ($page) => $page ->component('settings/workspace/Workspace', false) ->has('workspace') - ->where('canDelete', true) ->where('isOnlyWorkspace', false) ); }); @@ -198,7 +197,6 @@ $response->assertOk(); $response->assertInertia(fn ($page) => $page - ->where('canDelete', true) ->where('isOnlyWorkspace', true) ); }); From e8364407e7382cd6acdef10f44f883d2b6b3e52a Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 26 Jul 2026 12:55:46 -0400 Subject: [PATCH 03/32] Extract workspace delete danger zone into DeleteWorkspace component. Co-authored-by: Cursor --- .../components/settings/DeleteWorkspace.vue | 100 ++++++++++++++++++ .../js/components/settings/WorkspaceTab.vue | 84 ++------------- 2 files changed, 106 insertions(+), 78 deletions(-) create mode 100644 resources/js/components/settings/DeleteWorkspace.vue diff --git a/resources/js/components/settings/DeleteWorkspace.vue b/resources/js/components/settings/DeleteWorkspace.vue new file mode 100644 index 000000000..c3c18bb96 --- /dev/null +++ b/resources/js/components/settings/DeleteWorkspace.vue @@ -0,0 +1,100 @@ + + + diff --git a/resources/js/components/settings/WorkspaceTab.vue b/resources/js/components/settings/WorkspaceTab.vue index ca234120f..ac1f1c666 100644 --- a/resources/js/components/settings/WorkspaceTab.vue +++ b/resources/js/components/settings/WorkspaceTab.vue @@ -1,10 +1,8 @@ From 69d3f361c1ea29bc13217904f2a61678badaa66b Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 26 Jul 2026 12:57:38 -0400 Subject: [PATCH 04/32] Clarify workspace delete billing copy across locales. Co-authored-by: Cursor --- lang/ar/settings.php | 2 +- lang/de/settings.php | 2 +- lang/el/settings.php | 2 +- lang/en/settings.php | 2 +- lang/es/settings.php | 2 +- lang/fr/settings.php | 2 +- lang/it/settings.php | 2 +- lang/ja/settings.php | 2 +- lang/ko/settings.php | 2 +- lang/nl/settings.php | 2 +- lang/pl/settings.php | 2 +- lang/pt-BR/settings.php | 2 +- lang/ru/settings.php | 2 +- lang/tr/settings.php | 2 +- lang/zh/settings.php | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lang/ar/settings.php b/lang/ar/settings.php index bd302eadb..968026f6a 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'منطقة الخطر', 'danger_description' => 'إجراءات لا يمكن التراجع عنها.', 'delete_title' => 'حذف مساحة العمل هذه', - 'delete_description' => 'يحذف مساحة العمل هذه ومنشوراتها والحسابات المتصلة والوسائط نهائيًا. ستنخفض كمية اشتراكك بمقدار واحد.', + 'delete_description' => 'يحذف مساحة العمل هذه وكل ما فيها نهائيًا — المنشورات والحسابات المتصلة والوسائط. ستُحاسب على مساحة عمل واحدة أقل.', 'delete_description_self_hosted' => 'يحذف مساحة العمل هذه ومنشوراتها والحسابات المتصلة والوسائط نهائيًا.', 'delete_only_description' => 'لا يمكنك حذف مساحة العمل الوحيدة لديك. ألغِ اشتراكك في إعدادات الفوترة لإغلاق حسابك.', 'delete_go_to_billing' => 'الانتقال إلى الفوترة', diff --git a/lang/de/settings.php b/lang/de/settings.php index 63cd154b8..6ca01543b 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -145,7 +145,7 @@ 'danger_title' => 'Gefahrenzone', 'danger_description' => 'Unwiderrufliche Aktionen.', 'delete_title' => 'Diesen Workspace löschen', - 'delete_description' => 'Löscht diesen Workspace dauerhaft inklusive Beiträge, verbundener Konten und Medien. Die Menge deines Abonnements wird um eins verringert.', + 'delete_description' => 'Löscht diesen Workspace dauerhaft — inklusive Beiträge, verbundener Konten und Medien. Du wirst für einen Workspace weniger berechnet.', 'delete_description_self_hosted' => 'Löscht diesen Workspace dauerhaft inklusive Beiträge, verbundener Konten und Medien.', 'delete_only_description' => 'Du kannst deinen einzigen Workspace nicht löschen. Kündige dein Abonnement in den Abrechnungseinstellungen, um dein Konto zu schließen.', 'delete_go_to_billing' => 'Zur Abrechnung', diff --git a/lang/el/settings.php b/lang/el/settings.php index eb8313172..9637cba31 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Ζώνη κινδύνου', 'danger_description' => 'Μη αναστρέψιμες ενέργειες.', 'delete_title' => 'Διαγραφή αυτού του workspace', - 'delete_description' => 'Διαγράφει οριστικά αυτό το workspace, τις αναρτήσεις, τους συνδεδεμένους λογαριασμούς και τα μέσα. Η ποσότητα της συνδρομής σας θα μειωθεί κατά ένα.', + 'delete_description' => 'Διαγράφει οριστικά αυτό το workspace και ό,τι περιέχει — αναρτήσεις, συνδεδεμένους λογαριασμούς και μέσα. Θα χρεώνεστε για ένα λιγότερο workspace.', 'delete_description_self_hosted' => 'Διαγράφει οριστικά αυτό το workspace, τις αναρτήσεις, τους συνδεδεμένους λογαριασμούς και τα μέσα.', 'delete_only_description' => 'Δεν μπορείτε να διαγράψετε το μοναδικό σας workspace. Ακυρώστε τη συνδρομή σας στις ρυθμίσεις χρέωσης για να κλείσετε τον λογαριασμό σας.', 'delete_go_to_billing' => 'Μετάβαση στη χρέωση', diff --git a/lang/en/settings.php b/lang/en/settings.php index 5b93f5bd1..687212bc1 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Danger zone', 'danger_description' => 'Irreversible actions.', 'delete_title' => 'Delete this workspace', - 'delete_description' => 'Permanently deletes this workspace, its posts, connected accounts, and media. Your subscription quantity will decrease by one.', + 'delete_description' => 'Permanently deletes this workspace and everything in it — posts, connected accounts, and media. You will be charged for one fewer workspace.', 'delete_description_self_hosted' => 'Permanently deletes this workspace, its posts, connected accounts, and media.', 'delete_only_description' => 'You cannot delete your only workspace. Cancel your subscription in billing settings to close your account.', 'delete_go_to_billing' => 'Go to billing', diff --git a/lang/es/settings.php b/lang/es/settings.php index df33c2ca3..71020545f 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Zona de peligro', 'danger_description' => 'Acciones irreversibles.', 'delete_title' => 'Eliminar este workspace', - 'delete_description' => 'Elimina permanentemente este workspace, sus publicaciones, cuentas conectadas y medios. La cantidad de tu suscripción disminuirá en uno.', + 'delete_description' => 'Elimina este workspace de forma permanente, incluyendo publicaciones, cuentas conectadas y medios. Pasarás a pagar por un workspace menos.', 'delete_description_self_hosted' => 'Elimina permanentemente este workspace, sus publicaciones, cuentas conectadas y medios.', 'delete_only_description' => 'No puedes eliminar tu único workspace. Cancela tu suscripción en la configuración de facturación para cerrar tu cuenta.', 'delete_go_to_billing' => 'Ir a facturación', diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 478170cf0..8d4c984be 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Zone de danger', 'danger_description' => 'Actions irréversibles.', 'delete_title' => 'Supprimer cet espace de travail', - 'delete_description' => 'Supprime définitivement cet espace de travail, ses publications, comptes connectés et médias. La quantité de votre abonnement diminuera d’un.', + 'delete_description' => 'Supprime définitivement cet espace de travail et tout son contenu — publications, comptes connectés et médias. Vous serez facturé pour un espace de travail de moins.', 'delete_description_self_hosted' => 'Supprime définitivement cet espace de travail, ses publications, comptes connectés et médias.', 'delete_only_description' => 'Vous ne pouvez pas supprimer votre seul espace de travail. Annulez votre abonnement dans les paramètres de facturation pour fermer votre compte.', 'delete_go_to_billing' => 'Aller à la facturation', diff --git a/lang/it/settings.php b/lang/it/settings.php index 7a22b2c89..90e8ed274 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Zona pericolosa', 'danger_description' => 'Azioni irreversibili.', 'delete_title' => 'Elimina questo workspace', - 'delete_description' => 'Elimina definitivamente questo workspace, i suoi post, gli account collegati e i media. La quantità del tuo abbonamento diminuirà di uno.', + 'delete_description' => 'Elimina definitivamente questo workspace e tutto il suo contenuto — post, account collegati e media. Verrai addebitato per un workspace in meno.', 'delete_description_self_hosted' => 'Elimina definitivamente questo workspace, i suoi post, gli account collegati e i media.', 'delete_only_description' => 'Non puoi eliminare il tuo unico workspace. Annulla il tuo abbonamento nelle impostazioni di fatturazione per chiudere il tuo account.', 'delete_go_to_billing' => 'Vai alla fatturazione', diff --git a/lang/ja/settings.php b/lang/ja/settings.php index d9f5cbeca..5068c8527 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -143,7 +143,7 @@ 'danger_title' => '危険な操作', 'danger_description' => '元に戻せない操作です。', 'delete_title' => 'このワークスペースを削除', - 'delete_description' => 'このワークスペースと投稿、連携アカウント、メディアを完全に削除します。サブスクリプションの数量は1つ減ります。', + 'delete_description' => 'このワークスペースと、投稿・連携アカウント・メディアを含むすべての内容を完全に削除します。請求はワークスペースが1つ減った分になります。', 'delete_description_self_hosted' => 'このワークスペースと投稿、連携アカウント、メディアを完全に削除します。', 'delete_only_description' => '唯一のワークスペースは削除できません。アカウントを閉じるには、請求設定でサブスクリプションを解約してください。', 'delete_go_to_billing' => '請求へ移動', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 714663a95..070414947 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -143,7 +143,7 @@ 'danger_title' => '위험 구역', 'danger_description' => '되돌릴 수 없는 작업입니다.', 'delete_title' => '이 워크스페이스 삭제', - 'delete_description' => '이 워크스페이스와 게시물, 연결된 계정, 미디어를 영구 삭제합니다. 구독 수량이 하나 줄어듭니다.', + 'delete_description' => '이 워크스페이스와 그 안의 모든 것(게시물, 연결된 계정, 미디어)을 영구 삭제합니다. 워크스페이스 하나만큼 요금이 줄어듭니다.', 'delete_description_self_hosted' => '이 워크스페이스와 게시물, 연결된 계정, 미디어를 영구 삭제합니다.', 'delete_only_description' => '유일한 워크스페이스는 삭제할 수 없습니다. 계정을 닫으려면 결제 설정에서 구독을 취소하세요.', 'delete_go_to_billing' => '결제로 이동', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index dd9c16a42..a1f9c82f8 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Gevarenzone', 'danger_description' => 'Onomkeerbare acties.', 'delete_title' => 'Deze workspace verwijderen', - 'delete_description' => 'Verwijdert deze workspace permanent, inclusief posts, gekoppelde accounts en media. De hoeveelheid van je abonnement daalt met één.', + 'delete_description' => 'Verwijdert deze workspace permanent, inclusief posts, gekoppelde accounts en media. Je wordt voor één workspace minder gefactureerd.', 'delete_description_self_hosted' => 'Verwijdert deze workspace permanent, inclusief posts, gekoppelde accounts en media.', 'delete_only_description' => 'Je kunt je enige workspace niet verwijderen. Zeg je abonnement op in de facturatie-instellingen om je account te sluiten.', 'delete_go_to_billing' => 'Naar facturatie', diff --git a/lang/pl/settings.php b/lang/pl/settings.php index 365da11dc..f5195da9f 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Strefa zagrożenia', 'danger_description' => 'Nieodwracalne działania.', 'delete_title' => 'Usuń ten workspace', - 'delete_description' => 'Trwale usuwa ten workspace, jego posty, połączone konta i media. Ilość w subskrypcji zmniejszy się o jeden.', + 'delete_description' => 'Trwale usuwa ten workspace wraz z postami, połączonymi kontami i mediami. Będziesz rozliczany za o jeden workspace mniej.', 'delete_description_self_hosted' => 'Trwale usuwa ten workspace, jego posty, połączone konta i media.', 'delete_only_description' => 'Nie możesz usunąć swojego jedynego workspace. Anuluj subskrypcję w ustawieniach rozliczeń, aby zamknąć konto.', 'delete_go_to_billing' => 'Przejdź do rozliczeń', diff --git a/lang/pt-BR/settings.php b/lang/pt-BR/settings.php index 758f35b28..a1bb6b049 100644 --- a/lang/pt-BR/settings.php +++ b/lang/pt-BR/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Zona de perigo', 'danger_description' => 'Ações irreversíveis.', 'delete_title' => 'Excluir este workspace', - 'delete_description' => 'Exclui permanentemente este workspace, seus posts, contas conectadas e mídia. A quantidade da sua assinatura será reduzida em um.', + 'delete_description' => 'Exclui este workspace de forma permanente, incluindo posts, contas conectadas e mídia. Você passa a ser cobrado por um workspace a menos.', 'delete_description_self_hosted' => 'Exclui permanentemente este workspace, seus posts, contas conectadas e mídia.', 'delete_only_description' => 'Você não pode excluir seu único workspace. Cancele sua assinatura nas configurações de cobrança para encerrar sua conta.', 'delete_go_to_billing' => 'Ir para cobrança', diff --git a/lang/ru/settings.php b/lang/ru/settings.php index 235ae3355..df0ae082e 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -143,7 +143,7 @@ 'danger_title' => 'Опасная зона', 'danger_description' => 'Необратимые действия.', 'delete_title' => 'Удалить этот workspace', - 'delete_description' => 'Безвозвратно удаляет этот workspace, его посты, подключённые аккаунты и медиа. Количество в подписке уменьшится на один.', + 'delete_description' => 'Безвозвратно удаляет этот workspace и всё в нём — посты, подключённые аккаунты и медиа. Вам будут выставлять счёт на один workspace меньше.', 'delete_description_self_hosted' => 'Безвозвратно удаляет этот workspace, его посты, подключённые аккаунты и медиа.', 'delete_only_description' => 'Вы не можете удалить свой единственный workspace. Отмените подписку в настройках оплаты, чтобы закрыть аккаунт.', 'delete_go_to_billing' => 'Перейти к оплате', diff --git a/lang/tr/settings.php b/lang/tr/settings.php index ceef55a75..685ab13be 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -145,7 +145,7 @@ 'danger_title' => 'Tehlikeli bölge', 'danger_description' => 'Geri alınamaz işlemler.', 'delete_title' => 'Bu workspace’i sil', - 'delete_description' => 'Bu workspace’i, gönderilerini, bağlı hesaplarını ve medyasını kalıcı olarak siler. Abonelik miktarınız bir azalır.', + 'delete_description' => 'Bu workspace’i kalıcı olarak siler — gönderiler, bağlı hesaplar ve medya dahil. Bir workspace daha az ücretlendirilirsiniz.', 'delete_description_self_hosted' => 'Bu workspace’i, gönderilerini, bağlı hesaplarını ve medyasını kalıcı olarak siler.', 'delete_only_description' => 'Tek workspace’inizi silemezsiniz. Hesabınızı kapatmak için faturalandırma ayarlarından aboneliğinizi iptal edin.', 'delete_go_to_billing' => 'Faturalandırmaya git', diff --git a/lang/zh/settings.php b/lang/zh/settings.php index 6e27981ed..98e3fbfe9 100644 --- a/lang/zh/settings.php +++ b/lang/zh/settings.php @@ -143,7 +143,7 @@ 'danger_title' => '危险区域', 'danger_description' => '不可撤销的操作。', 'delete_title' => '删除此工作区', - 'delete_description' => '永久删除此工作区及其帖子、已连接账户和媒体。订阅数量将减少一个。', + 'delete_description' => '永久删除此工作区及其全部内容(帖子、已连接账户和媒体)。之后将按少一个工作区计费。', 'delete_description_self_hosted' => '永久删除此工作区及其帖子、已连接账户和媒体。', 'delete_only_description' => '无法删除唯一的工作区。请在账单设置中取消订阅以关闭账户。', 'delete_go_to_billing' => '前往账单', From d292ec3840dccd220bb73f9be8c5eab17d415bc6 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 26 Jul 2026 12:58:53 -0400 Subject: [PATCH 05/32] Match workspace delete card to the delete-account settings pattern. Co-authored-by: Cursor --- .../components/settings/DeleteWorkspace.vue | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/resources/js/components/settings/DeleteWorkspace.vue b/resources/js/components/settings/DeleteWorkspace.vue index c3c18bb96..311b61a9f 100644 --- a/resources/js/components/settings/DeleteWorkspace.vue +++ b/resources/js/components/settings/DeleteWorkspace.vue @@ -1,6 +1,5 @@ From 7f34cfbd4f5a5655c622d47a7f834e318b09a70a Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 26 Jul 2026 13:10:34 -0400 Subject: [PATCH 06/32] Harden workspace and account deletion around shared members. Enforce owner-only workspace creation, rehome stranded members to a personal account, warn about member access loss, and clarify the only-workspace SaaS exit paths. Co-authored-by: Cursor --- app/Actions/User/EnsurePersonalAccount.php | 45 ++++++++++ app/Actions/Workspace/DeleteWorkspace.php | 10 +++ .../App/Settings/ProfileController.php | 8 ++ .../Controllers/App/WorkspaceController.php | 5 ++ .../App/Workspace/StoreWorkspaceRequest.php | 3 +- lang/ar/settings.php | 4 +- lang/de/settings.php | 4 +- lang/el/settings.php | 4 +- lang/en/settings.php | 4 +- lang/es/settings.php | 4 +- lang/fr/settings.php | 4 +- lang/it/settings.php | 4 +- lang/ja/settings.php | 4 +- lang/ko/settings.php | 4 +- lang/nl/settings.php | 4 +- lang/pl/settings.php | 4 +- lang/pt-BR/settings.php | 4 +- lang/ru/settings.php | 4 +- lang/tr/settings.php | 4 +- lang/zh/settings.php | 4 +- .../components/settings/DeleteWorkspace.vue | 89 ++++++++++++------- .../js/components/settings/WorkspaceTab.vue | 2 + .../js/pages/settings/workspace/Workspace.vue | 2 + .../DeleteWorkspaceMemberRehomeTest.php | 65 ++++++++++++++ .../WorkspaceRolePermissionsTest.php | 26 ++++++ tests/Feature/Settings/ProfileUpdateTest.php | 27 ++++++ tests/Feature/WorkspaceControllerTest.php | 1 + .../User/EnsurePersonalAccountTest.php | 44 +++++++++ 28 files changed, 341 insertions(+), 46 deletions(-) create mode 100644 app/Actions/User/EnsurePersonalAccount.php create mode 100644 tests/Feature/Actions/Workspace/DeleteWorkspaceMemberRehomeTest.php create mode 100644 tests/Unit/Actions/User/EnsurePersonalAccountTest.php diff --git a/app/Actions/User/EnsurePersonalAccount.php b/app/Actions/User/EnsurePersonalAccount.php new file mode 100644 index 000000000..2862afc36 --- /dev/null +++ b/app/Actions/User/EnsurePersonalAccount.php @@ -0,0 +1,45 @@ +account_id && $user->isAccountOwner()) { + return $user->account; + } + + $personalAccount = Account::query() + ->where('owner_id', $user->id) + ->when( + $user->account_id, + fn ($query) => $query->where('id', '!=', $user->account_id), + ) + ->first(); + + if (! $personalAccount) { + $personalAccount = Account::create([ + 'name' => "{$user->name}'s Account", + 'billing_email' => $user->email, + 'owner_id' => $user->id, + ]); + } + + $user->update(['account_id' => $personalAccount->id]); + + return $personalAccount; + } +} diff --git a/app/Actions/Workspace/DeleteWorkspace.php b/app/Actions/Workspace/DeleteWorkspace.php index 57a545b3b..739994e71 100644 --- a/app/Actions/Workspace/DeleteWorkspace.php +++ b/app/Actions/Workspace/DeleteWorkspace.php @@ -4,6 +4,7 @@ namespace App\Actions\Workspace; +use App\Actions\User\EnsurePersonalAccount; use App\Jobs\PostHog\SyncAccountUsage; use App\Models\User; use App\Models\Workspace; @@ -28,6 +29,15 @@ public static function execute(User $user, Workspace $workspace): void $workspace->delete(); + if ($account) { + User::query() + ->where('account_id', $account->id) + ->where('id', '!=', $account->owner_id) + ->whereDoesntHave('workspaces') + ->get() + ->each(fn (User $stranded) => EnsurePersonalAccount::execute($stranded)); + } + $account?->syncWorkspaceQuantity(); if (PostHogService::isEnabled()) { diff --git a/app/Http/Controllers/App/Settings/ProfileController.php b/app/Http/Controllers/App/Settings/ProfileController.php index 24d05741e..8082b7d8d 100644 --- a/app/Http/Controllers/App/Settings/ProfileController.php +++ b/app/Http/Controllers/App/Settings/ProfileController.php @@ -4,10 +4,12 @@ namespace App\Http\Controllers\App\Settings; +use App\Actions\User\EnsurePersonalAccount; use App\Http\Controllers\Controller; use App\Http\Requests\App\Settings\ProfileDeleteRequest; use App\Http\Requests\App\Settings\ProfileUpdateRequest; use App\Models\Account; +use App\Models\User; use App\Models\Workspace; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Http\RedirectResponse; @@ -116,6 +118,12 @@ public function destroy(ProfileDeleteRequest $request): RedirectResponse $user->workspaces()->detach(); if ($account && $isOwner) { + User::query() + ->where('account_id', $account->id) + ->where('id', '!=', $user->id) + ->get() + ->each(fn (User $member) => EnsurePersonalAccount::execute($member)); + if ($account->subscribed(Account::SUBSCRIPTION_NAME)) { $account->subscription(Account::SUBSCRIPTION_NAME)->cancelNow(); } diff --git a/app/Http/Controllers/App/WorkspaceController.php b/app/Http/Controllers/App/WorkspaceController.php index 9fd70bc97..ad2ee90dc 100644 --- a/app/Http/Controllers/App/WorkspaceController.php +++ b/app/Http/Controllers/App/WorkspaceController.php @@ -67,6 +67,8 @@ public function index(Request $request): Response public function create(Request $request): Response|RedirectResponse { + $this->authorize('create', Workspace::class); + if ($redirect = $this->denyAdditionalWorkspaceWithoutSubscription($request->user())) { return $redirect; } @@ -156,6 +158,9 @@ public function settings(Request $request): Response|RedirectResponse 'workspace' => $workspace, 'isOnlyWorkspace' => ! config('trypost.self_hosted') && $workspace->account->workspaces()->count() <= 1, + 'otherMemberCount' => $workspace->members() + ->where('users.id', '!=', $user->id) + ->count(), ]); } diff --git a/app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php b/app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php index 4c5715965..e8cda84b5 100644 --- a/app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php +++ b/app/Http/Requests/App/Workspace/StoreWorkspaceRequest.php @@ -8,6 +8,7 @@ use App\Enums\Workspace\BrandVoiceTrait; use App\Enums\Workspace\ContentLanguage; use App\Enums\Workspace\ImageStyle; +use App\Models\Workspace; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -15,7 +16,7 @@ class StoreWorkspaceRequest extends FormRequest { public function authorize(): bool { - return true; + return $this->user()->can('create', Workspace::class); } public function rules(): array diff --git a/lang/ar/settings.php b/lang/ar/settings.php index 968026f6a..03fdccfb1 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'حذف مساحة العمل هذه', 'delete_description' => 'يحذف مساحة العمل هذه وكل ما فيها نهائيًا — المنشورات والحسابات المتصلة والوسائط. ستُحاسب على مساحة عمل واحدة أقل.', 'delete_description_self_hosted' => 'يحذف مساحة العمل هذه ومنشوراتها والحسابات المتصلة والوسائط نهائيًا.', - 'delete_only_description' => 'لا يمكنك حذف مساحة العمل الوحيدة لديك. ألغِ اشتراكك في إعدادات الفوترة لإغلاق حسابك.', + 'delete_only_description' => 'لا يمكنك حذف مساحة العمل الوحيدة لديك. ألغِ الاشتراك في الفوترة لإيقاف الدفع، أو احذف حسابك في المصادقة لإلغاء الفوترة وإزالة كل شيء نهائيًا.', 'delete_go_to_billing' => 'الانتقال إلى الفوترة', + 'delete_go_to_delete_account' => 'حذف الحساب', + 'delete_members_warning' => '{1}سيفقد عضو آخر واحد الوصول.|[2,*]سيفقد :count أعضاء آخرين الوصول.', 'delete_action' => 'حذف مساحة العمل', 'delete_cancel' => 'إلغاء', 'delete_confirm_title' => 'حذف مساحة العمل؟', diff --git a/lang/de/settings.php b/lang/de/settings.php index 6ca01543b..75d430272 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -147,8 +147,10 @@ 'delete_title' => 'Diesen Workspace löschen', 'delete_description' => 'Löscht diesen Workspace dauerhaft — inklusive Beiträge, verbundener Konten und Medien. Du wirst für einen Workspace weniger berechnet.', 'delete_description_self_hosted' => 'Löscht diesen Workspace dauerhaft inklusive Beiträge, verbundener Konten und Medien.', - 'delete_only_description' => 'Du kannst deinen einzigen Workspace nicht löschen. Kündige dein Abonnement in den Abrechnungseinstellungen, um dein Konto zu schließen.', + 'delete_only_description' => 'Du kannst deinen einzigen Workspace nicht löschen. Kündige das Abonnement in der Abrechnung, um die Zahlung zu stoppen, oder lösche dein Konto unter Authentifizierung, um die Abrechnung zu beenden und alles dauerhaft zu entfernen.', 'delete_go_to_billing' => 'Zur Abrechnung', + 'delete_go_to_delete_account' => 'Konto löschen', + 'delete_members_warning' => '{1}:count weiteres Mitglied verliert den Zugriff.|[2,*]:count weitere Mitglieder verlieren den Zugriff.', 'delete_action' => 'Workspace löschen', 'delete_cancel' => 'Abbrechen', 'delete_confirm_title' => 'Workspace löschen?', diff --git a/lang/el/settings.php b/lang/el/settings.php index 9637cba31..5bee4dc3d 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Διαγραφή αυτού του workspace', 'delete_description' => 'Διαγράφει οριστικά αυτό το workspace και ό,τι περιέχει — αναρτήσεις, συνδεδεμένους λογαριασμούς και μέσα. Θα χρεώνεστε για ένα λιγότερο workspace.', 'delete_description_self_hosted' => 'Διαγράφει οριστικά αυτό το workspace, τις αναρτήσεις, τους συνδεδεμένους λογαριασμούς και τα μέσα.', - 'delete_only_description' => 'Δεν μπορείτε να διαγράψετε το μοναδικό σας workspace. Ακυρώστε τη συνδρομή σας στις ρυθμίσεις χρέωσης για να κλείσετε τον λογαριασμό σας.', + 'delete_only_description' => 'Δεν μπορείτε να διαγράψετε το μοναδικό σας workspace. Ακυρώστε τη συνδρομή στη χρέωση για να σταματήσετε να πληρώνετε, ή διαγράψτε τον λογαριασμό σας στην Επαλήθευση για να ακυρώσετε τη χρέωση και να αφαιρέσετε τα πάντα οριστικά.', 'delete_go_to_billing' => 'Μετάβαση στη χρέωση', + 'delete_go_to_delete_account' => 'Διαγραφή λογαριασμού', + 'delete_members_warning' => '{1}:count άλλο μέλος θα χάσει την πρόσβαση.|[2,*]:count άλλα μέλη θα χάσουν την πρόσβαση.', 'delete_action' => 'Διαγραφή workspace', 'delete_cancel' => 'Ακύρωση', 'delete_confirm_title' => 'Διαγραφή workspace;', diff --git a/lang/en/settings.php b/lang/en/settings.php index 687212bc1..646c2f49a 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Delete this workspace', 'delete_description' => 'Permanently deletes this workspace and everything in it — posts, connected accounts, and media. You will be charged for one fewer workspace.', 'delete_description_self_hosted' => 'Permanently deletes this workspace, its posts, connected accounts, and media.', - 'delete_only_description' => 'You cannot delete your only workspace. Cancel your subscription in billing settings to close your account.', + 'delete_only_description' => 'You cannot delete your only workspace. Cancel your subscription in billing to stop paying, or delete your account in Authentication settings to cancel billing and permanently remove everything.', 'delete_go_to_billing' => 'Go to billing', + 'delete_go_to_delete_account' => 'Delete account', + 'delete_members_warning' => '{1}:count other member will lose access.|[2,*]:count other members will lose access.', 'delete_action' => 'Delete workspace', 'delete_cancel' => 'Cancel', 'delete_confirm_title' => 'Delete workspace?', diff --git a/lang/es/settings.php b/lang/es/settings.php index 71020545f..f2957caab 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Eliminar este workspace', 'delete_description' => 'Elimina este workspace de forma permanente, incluyendo publicaciones, cuentas conectadas y medios. Pasarás a pagar por un workspace menos.', 'delete_description_self_hosted' => 'Elimina permanentemente este workspace, sus publicaciones, cuentas conectadas y medios.', - 'delete_only_description' => 'No puedes eliminar tu único workspace. Cancela tu suscripción en la configuración de facturación para cerrar tu cuenta.', + 'delete_only_description' => 'No puedes eliminar tu único workspace. Cancela la suscripción en facturación para dejar de pagar, o elimina tu cuenta en Autenticación para cancelar la facturación y borrar todo de forma permanente.', 'delete_go_to_billing' => 'Ir a facturación', + 'delete_go_to_delete_account' => 'Eliminar cuenta', + 'delete_members_warning' => '{1}:count miembro más perderá el acceso.|[2,*]:count miembros más perderán el acceso.', 'delete_action' => 'Eliminar workspace', 'delete_cancel' => 'Cancelar', 'delete_confirm_title' => '¿Eliminar workspace?', diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 8d4c984be..703348974 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Supprimer cet espace de travail', 'delete_description' => 'Supprime définitivement cet espace de travail et tout son contenu — publications, comptes connectés et médias. Vous serez facturé pour un espace de travail de moins.', 'delete_description_self_hosted' => 'Supprime définitivement cet espace de travail, ses publications, comptes connectés et médias.', - 'delete_only_description' => 'Vous ne pouvez pas supprimer votre seul espace de travail. Annulez votre abonnement dans les paramètres de facturation pour fermer votre compte.', + 'delete_only_description' => 'Vous ne pouvez pas supprimer votre seul espace de travail. Annulez l’abonnement dans la facturation pour arrêter de payer, ou supprimez votre compte dans Authentification pour annuler la facturation et tout supprimer définitivement.', 'delete_go_to_billing' => 'Aller à la facturation', + 'delete_go_to_delete_account' => 'Supprimer le compte', + 'delete_members_warning' => '{1}:count autre membre perdra l’accès.|[2,*]:count autres membres perdront l’accès.', 'delete_action' => 'Supprimer l’espace de travail', 'delete_cancel' => 'Annuler', 'delete_confirm_title' => 'Supprimer l’espace de travail ?', diff --git a/lang/it/settings.php b/lang/it/settings.php index 90e8ed274..6f4aa3413 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Elimina questo workspace', 'delete_description' => 'Elimina definitivamente questo workspace e tutto il suo contenuto — post, account collegati e media. Verrai addebitato per un workspace in meno.', 'delete_description_self_hosted' => 'Elimina definitivamente questo workspace, i suoi post, gli account collegati e i media.', - 'delete_only_description' => 'Non puoi eliminare il tuo unico workspace. Annulla il tuo abbonamento nelle impostazioni di fatturazione per chiudere il tuo account.', + 'delete_only_description' => 'Non puoi eliminare il tuo unico workspace. Annulla l’abbonamento nella fatturazione per smettere di pagare, oppure elimina il tuo account in Autenticazione per annullare la fatturazione e rimuovere tutto in modo permanente.', 'delete_go_to_billing' => 'Vai alla fatturazione', + 'delete_go_to_delete_account' => 'Elimina account', + 'delete_members_warning' => '{1}:count altro membro perderà l’accesso.|[2,*]:count altri membri perderanno l’accesso.', 'delete_action' => 'Elimina workspace', 'delete_cancel' => 'Annulla', 'delete_confirm_title' => 'Eliminare il workspace?', diff --git a/lang/ja/settings.php b/lang/ja/settings.php index 5068c8527..35180d91b 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'このワークスペースを削除', 'delete_description' => 'このワークスペースと、投稿・連携アカウント・メディアを含むすべての内容を完全に削除します。請求はワークスペースが1つ減った分になります。', 'delete_description_self_hosted' => 'このワークスペースと投稿、連携アカウント、メディアを完全に削除します。', - 'delete_only_description' => '唯一のワークスペースは削除できません。アカウントを閉じるには、請求設定でサブスクリプションを解約してください。', + 'delete_only_description' => '唯一のワークスペースは削除できません。支払いを止めるには請求設定でサブスクリプションを解約するか、認証設定でアカウントを削除して請求を止め、すべてを完全に削除してください。', 'delete_go_to_billing' => '請求へ移動', + 'delete_go_to_delete_account' => 'アカウントを削除', + 'delete_members_warning' => '{1}他のメンバー :count 人がアクセスできなくなります。|[2,*]他のメンバー :count 人がアクセスできなくなります。', 'delete_action' => 'ワークスペースを削除', 'delete_cancel' => 'キャンセル', 'delete_confirm_title' => 'ワークスペースを削除しますか?', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 070414947..551ce44e0 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -145,8 +145,10 @@ 'delete_title' => '이 워크스페이스 삭제', 'delete_description' => '이 워크스페이스와 그 안의 모든 것(게시물, 연결된 계정, 미디어)을 영구 삭제합니다. 워크스페이스 하나만큼 요금이 줄어듭니다.', 'delete_description_self_hosted' => '이 워크스페이스와 게시물, 연결된 계정, 미디어를 영구 삭제합니다.', - 'delete_only_description' => '유일한 워크스페이스는 삭제할 수 없습니다. 계정을 닫으려면 결제 설정에서 구독을 취소하세요.', + 'delete_only_description' => '유일한 워크스페이스는 삭제할 수 없습니다. 결제를 멈추려면 청구에서 구독을 취소하거나, 인증 설정에서 계정을 삭제해 결제를 취소하고 모든 데이터를 영구 삭제하세요.', 'delete_go_to_billing' => '결제로 이동', + 'delete_go_to_delete_account' => '계정 삭제', + 'delete_members_warning' => '{1}다른 멤버 :count명이 접근 권한을 잃습니다.|[2,*]다른 멤버 :count명이 접근 권한을 잃습니다.', 'delete_action' => '워크스페이스 삭제', 'delete_cancel' => '취소', 'delete_confirm_title' => '워크스페이스를 삭제할까요?', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index a1f9c82f8..886e8aa89 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Deze workspace verwijderen', 'delete_description' => 'Verwijdert deze workspace permanent, inclusief posts, gekoppelde accounts en media. Je wordt voor één workspace minder gefactureerd.', 'delete_description_self_hosted' => 'Verwijdert deze workspace permanent, inclusief posts, gekoppelde accounts en media.', - 'delete_only_description' => 'Je kunt je enige workspace niet verwijderen. Zeg je abonnement op in de facturatie-instellingen om je account te sluiten.', + 'delete_only_description' => 'Je kunt je enige workspace niet verwijderen. Zeg je abonnement op in facturatie om te stoppen met betalen, of verwijder je account bij Authenticatie om de facturatie te annuleren en alles permanent te wissen.', 'delete_go_to_billing' => 'Naar facturatie', + 'delete_go_to_delete_account' => 'Account verwijderen', + 'delete_members_warning' => '{1}:count ander lid verliest toegang.|[2,*]:count andere leden verliezen toegang.', 'delete_action' => 'Workspace verwijderen', 'delete_cancel' => 'Annuleren', 'delete_confirm_title' => 'Workspace verwijderen?', diff --git a/lang/pl/settings.php b/lang/pl/settings.php index f5195da9f..b68507d0c 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Usuń ten workspace', 'delete_description' => 'Trwale usuwa ten workspace wraz z postami, połączonymi kontami i mediami. Będziesz rozliczany za o jeden workspace mniej.', 'delete_description_self_hosted' => 'Trwale usuwa ten workspace, jego posty, połączone konta i media.', - 'delete_only_description' => 'Nie możesz usunąć swojego jedynego workspace. Anuluj subskrypcję w ustawieniach rozliczeń, aby zamknąć konto.', + 'delete_only_description' => 'Nie możesz usunąć swojego jedynego workspace. Anuluj subskrypcję w rozliczeniach, aby przestać płacić, albo usuń konto w Uwierzytelnianiu, aby anulować rozliczenia i trwale usunąć wszystko.', 'delete_go_to_billing' => 'Przejdź do rozliczeń', + 'delete_go_to_delete_account' => 'Usuń konto', + 'delete_members_warning' => '{1}:count inny członek straci dostęp.|[2,*]:count innych członków straci dostęp.', 'delete_action' => 'Usuń workspace', 'delete_cancel' => 'Anuluj', 'delete_confirm_title' => 'Usunąć workspace?', diff --git a/lang/pt-BR/settings.php b/lang/pt-BR/settings.php index a1bb6b049..54b0b9d68 100644 --- a/lang/pt-BR/settings.php +++ b/lang/pt-BR/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Excluir este workspace', 'delete_description' => 'Exclui este workspace de forma permanente, incluindo posts, contas conectadas e mídia. Você passa a ser cobrado por um workspace a menos.', 'delete_description_self_hosted' => 'Exclui permanentemente este workspace, seus posts, contas conectadas e mídia.', - 'delete_only_description' => 'Você não pode excluir seu único workspace. Cancele sua assinatura nas configurações de cobrança para encerrar sua conta.', + 'delete_only_description' => 'Você não pode excluir seu único workspace. Cancele a assinatura na cobrança para parar de pagar, ou exclua sua conta em Autenticação para cancelar a cobrança e remover tudo permanentemente.', 'delete_go_to_billing' => 'Ir para cobrança', + 'delete_go_to_delete_account' => 'Excluir conta', + 'delete_members_warning' => '{1}:count outro membro perderá o acesso.|[2,*]:count outros membros perderão o acesso.', 'delete_action' => 'Excluir workspace', 'delete_cancel' => 'Cancelar', 'delete_confirm_title' => 'Excluir workspace?', diff --git a/lang/ru/settings.php b/lang/ru/settings.php index df0ae082e..6f3e3d8ea 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -145,8 +145,10 @@ 'delete_title' => 'Удалить этот workspace', 'delete_description' => 'Безвозвратно удаляет этот workspace и всё в нём — посты, подключённые аккаунты и медиа. Вам будут выставлять счёт на один workspace меньше.', 'delete_description_self_hosted' => 'Безвозвратно удаляет этот workspace, его посты, подключённые аккаунты и медиа.', - 'delete_only_description' => 'Вы не можете удалить свой единственный workspace. Отмените подписку в настройках оплаты, чтобы закрыть аккаунт.', + 'delete_only_description' => 'Вы не можете удалить свой единственный workspace. Отмените подписку в разделе оплаты, чтобы перестать платить, или удалите аккаунт в Аутентификации, чтобы отменить оплату и навсегда удалить всё.', 'delete_go_to_billing' => 'Перейти к оплате', + 'delete_go_to_delete_account' => 'Удалить аккаунт', + 'delete_members_warning' => '{1}:count другой участник потеряет доступ.|[2,*]:count других участников потеряют доступ.', 'delete_action' => 'Удалить workspace', 'delete_cancel' => 'Отмена', 'delete_confirm_title' => 'Удалить workspace?', diff --git a/lang/tr/settings.php b/lang/tr/settings.php index 685ab13be..ece214c68 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -147,8 +147,10 @@ 'delete_title' => 'Bu workspace’i sil', 'delete_description' => 'Bu workspace’i kalıcı olarak siler — gönderiler, bağlı hesaplar ve medya dahil. Bir workspace daha az ücretlendirilirsiniz.', 'delete_description_self_hosted' => 'Bu workspace’i, gönderilerini, bağlı hesaplarını ve medyasını kalıcı olarak siler.', - 'delete_only_description' => 'Tek workspace’inizi silemezsiniz. Hesabınızı kapatmak için faturalandırma ayarlarından aboneliğinizi iptal edin.', + 'delete_only_description' => 'Tek workspace’inizi silemezsiniz. Ödemeyi durdurmak için faturalandırmadan aboneliği iptal edin veya her şeyi kalıcı olarak silmek için Kimlik doğrulama ayarlarından hesabınızı silin.', 'delete_go_to_billing' => 'Faturalandırmaya git', + 'delete_go_to_delete_account' => 'Hesabı sil', + 'delete_members_warning' => '{1}:count diğer üye erişimi kaybedecek.|[2,*]:count diğer üye erişimi kaybedecek.', 'delete_action' => 'Workspace’i sil', 'delete_cancel' => 'İptal', 'delete_confirm_title' => 'Workspace silinsin mi?', diff --git a/lang/zh/settings.php b/lang/zh/settings.php index 98e3fbfe9..fb4a3d051 100644 --- a/lang/zh/settings.php +++ b/lang/zh/settings.php @@ -145,8 +145,10 @@ 'delete_title' => '删除此工作区', 'delete_description' => '永久删除此工作区及其全部内容(帖子、已连接账户和媒体)。之后将按少一个工作区计费。', 'delete_description_self_hosted' => '永久删除此工作区及其帖子、已连接账户和媒体。', - 'delete_only_description' => '无法删除唯一的工作区。请在账单设置中取消订阅以关闭账户。', + 'delete_only_description' => '无法删除唯一的工作区。可在账单中取消订阅以停止付费,或在身份验证设置中删除账户以取消计费并永久删除全部数据。', 'delete_go_to_billing' => '前往账单', + 'delete_go_to_delete_account' => '删除账户', + 'delete_members_warning' => '{1}另有 :count 位成员将失去访问权限。|[2,*]另有 :count 位成员将失去访问权限。', 'delete_action' => '删除工作区', 'delete_cancel' => '取消', 'delete_confirm_title' => '删除工作区?', diff --git a/resources/js/components/settings/DeleteWorkspace.vue b/resources/js/components/settings/DeleteWorkspace.vue index 311b61a9f..253b9e4c8 100644 --- a/resources/js/components/settings/DeleteWorkspace.vue +++ b/resources/js/components/settings/DeleteWorkspace.vue @@ -1,11 +1,13 @@ @@ -75,6 +76,7 @@ defineProps<{ diff --git a/resources/js/pages/settings/workspace/Workspace.vue b/resources/js/pages/settings/workspace/Workspace.vue index e9ee5c788..56db8152d 100644 --- a/resources/js/pages/settings/workspace/Workspace.vue +++ b/resources/js/pages/settings/workspace/Workspace.vue @@ -25,6 +25,7 @@ interface Workspace { defineProps<{ workspace: Workspace; isOnlyWorkspace: boolean; + otherMemberCount: number; }>(); const tabs = computed(() => [ @@ -50,6 +51,7 @@ const tabs = computed(() => [ diff --git a/tests/Feature/Actions/Workspace/DeleteWorkspaceMemberRehomeTest.php b/tests/Feature/Actions/Workspace/DeleteWorkspaceMemberRehomeTest.php new file mode 100644 index 000000000..5ba9c4e83 --- /dev/null +++ b/tests/Feature/Actions/Workspace/DeleteWorkspaceMemberRehomeTest.php @@ -0,0 +1,65 @@ +create(); + $member = User::factory()->create(); + $personalAccountId = $member->account_id; + $member->update(['account_id' => $owner->account_id]); + + $workspace = Workspace::factory()->create([ + 'account_id' => $owner->account_id, + 'user_id' => $owner->id, + ]); + Workspace::factory()->create([ + 'account_id' => $owner->account_id, + 'user_id' => $owner->id, + ]); + + $workspace->members()->attach($owner->id, ['role' => Role::Admin->value]); + $workspace->members()->attach($member->id, ['role' => Role::Member->value]); + $member->update(['current_workspace_id' => $workspace->id]); + + DeleteWorkspace::execute($owner, $workspace); + + $member->refresh(); + + expect($member->account_id)->toBe($personalAccountId); + expect($member->current_workspace_id)->toBeNull(); + expect($member->isAccountOwner())->toBeTrue(); +}); + +test('delete workspace does not rehome members who still have another workspace', function () { + $owner = User::factory()->create(); + $member = User::factory()->create(); + $member->update(['account_id' => $owner->account_id]); + + $first = Workspace::factory()->create([ + 'account_id' => $owner->account_id, + 'user_id' => $owner->id, + ]); + $second = Workspace::factory()->create([ + 'account_id' => $owner->account_id, + 'user_id' => $owner->id, + ]); + + foreach ([$first, $second] as $workspace) { + $workspace->members()->attach($owner->id, ['role' => Role::Admin->value]); + $workspace->members()->attach($member->id, ['role' => Role::Member->value]); + } + + $member->update(['current_workspace_id' => $first->id]); + $sharedAccountId = $owner->account_id; + + DeleteWorkspace::execute($owner, $first); + + $member->refresh(); + + expect($member->account_id)->toBe($sharedAccountId); + expect($member->current_workspace_id)->toBe($second->id); +}); diff --git a/tests/Feature/Permissions/WorkspaceRolePermissionsTest.php b/tests/Feature/Permissions/WorkspaceRolePermissionsTest.php index 9fdf73338..3e32306ad 100644 --- a/tests/Feature/Permissions/WorkspaceRolePermissionsTest.php +++ b/tests/Feature/Permissions/WorkspaceRolePermissionsTest.php @@ -134,3 +134,29 @@ ->post(route('app.posts.store')) ->assertRedirect(route('app.accounts')); }); + +test('a member cannot open the create workspace form', function () { + $this->actingAs($this->member) + ->get(route('app.workspaces.create')) + ->assertForbidden(); +}); + +test('a member cannot store a workspace on the shared account', function () { + $this->actingAs($this->member) + ->post(route('app.workspaces.store'), [ + 'name' => 'Sneaky Workspace', + ]) + ->assertForbidden(); + + expect(Workspace::where('name', 'Sneaky Workspace')->exists())->toBeFalse(); +}); + +test('a workspace admin cannot store a workspace on the shared account', function () { + $this->actingAs($this->admin) + ->post(route('app.workspaces.store'), [ + 'name' => 'Admin Workspace', + ]) + ->assertForbidden(); + + expect(Workspace::where('name', 'Admin Workspace')->exists())->toBeFalse(); +}); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index 60786517a..0de74e9ea 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -294,6 +294,33 @@ expect($workspace->fresh()->members()->where('users.id', $member->id)->exists())->toBeFalse(); })->with([true, false]); +test('owner deleting profile rehomes remaining members to a personal account', function () { + $owner = User::factory()->create(); + $member = User::factory()->create(); + $personalAccountId = $member->account_id; + $member->update(['account_id' => $owner->account_id]); + $accountId = $owner->account_id; + + $workspace = Workspace::factory()->create([ + 'account_id' => $accountId, + 'user_id' => $owner->id, + ]); + $workspace->members()->attach($owner->id, ['role' => Role::Member->value]); + $workspace->members()->attach($member->id, ['role' => Role::Member->value]); + $member->update(['current_workspace_id' => $workspace->id]); + + $this->actingAs($owner)->delete(route('app.profile.destroy'), [ + 'password' => 'password', + ]); + + $member->refresh(); + + expect(Account::find($accountId))->toBeNull(); + expect($member->account_id)->toBe($personalAccountId); + expect($member->isAccountOwner())->toBeTrue(); + expect($member->current_workspace_id)->toBeNull(); +}); + test('owner deleting profile destroys the account and cascades', function (bool $selfHosted) { config()->set('trypost.self_hosted', $selfHosted); diff --git a/tests/Feature/WorkspaceControllerTest.php b/tests/Feature/WorkspaceControllerTest.php index b8cd35de2..7bdb6c333 100644 --- a/tests/Feature/WorkspaceControllerTest.php +++ b/tests/Feature/WorkspaceControllerTest.php @@ -181,6 +181,7 @@ ->component('settings/workspace/Workspace', false) ->has('workspace') ->where('isOnlyWorkspace', false) + ->where('otherMemberCount', 0) ); }); diff --git a/tests/Unit/Actions/User/EnsurePersonalAccountTest.php b/tests/Unit/Actions/User/EnsurePersonalAccountTest.php new file mode 100644 index 000000000..aee0f9aef --- /dev/null +++ b/tests/Unit/Actions/User/EnsurePersonalAccountTest.php @@ -0,0 +1,44 @@ +create(); + + $account = EnsurePersonalAccount::execute($user); + + expect($account->id)->toBe($user->account_id); + expect($account->owner_id)->toBe($user->id); + expect(Account::where('owner_id', $user->id)->count())->toBe(1); +}); + +test('reclaims an orphaned personal account when the user joined another account', function () { + $owner = User::factory()->create(); + $member = User::factory()->create(); + $personalAccountId = $member->account_id; + + $member->update(['account_id' => $owner->account_id]); + + $account = EnsurePersonalAccount::execute($member->fresh()); + + expect($account->id)->toBe($personalAccountId); + expect($member->fresh()->account_id)->toBe($personalAccountId); + expect($account->owner_id)->toBe($member->id); +}); + +test('creates a personal account when the user has none of their own', function () { + $owner = User::factory()->create(); + $member = User::factory()->create(['account_id' => $owner->account_id]); + Account::where('owner_id', $member->id)->delete(); + $member->update(['account_id' => $owner->account_id]); + + $account = EnsurePersonalAccount::execute($member->fresh()); + + expect($member->fresh()->account_id)->toBe($account->id); + expect($account->owner_id)->toBe($member->id); + expect($account->billing_email)->toBe($member->email); +}); From 124a7b84e1ddfb7d4dce7af752a0813a9814d661 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sun, 26 Jul 2026 13:34:55 -0400 Subject: [PATCH 07/32] Harden workspace delete: owner-only billing impact and safer member rehome. Restrict delete to account owners, rehome stranded members transactionally with account-scoped fallbacks, and clean up the danger-zone UI/copy. Co-authored-by: Cursor --- app/Actions/User/EnsurePersonalAccount.php | 46 +++++++++++++++++++ app/Actions/Workspace/DeleteWorkspace.php | 43 +++++++++-------- .../App/Settings/ProfileController.php | 30 ++++++------ .../Controllers/App/WorkspaceController.php | 12 +++-- app/Policies/WorkspacePolicy.php | 7 +-- lang/ar/settings.php | 3 +- lang/de/settings.php | 3 +- lang/el/settings.php | 3 +- lang/en/settings.php | 3 +- lang/es/settings.php | 3 +- lang/fr/settings.php | 3 +- lang/it/settings.php | 3 +- lang/ja/settings.php | 3 +- lang/ko/settings.php | 3 +- lang/nl/settings.php | 3 +- lang/pl/settings.php | 3 +- lang/pt-BR/settings.php | 3 +- lang/ru/settings.php | 3 +- lang/tr/settings.php | 3 +- lang/zh/settings.php | 3 +- .../components/settings/DeleteWorkspace.vue | 4 +- .../js/components/settings/WorkspaceTab.vue | 17 ++++--- .../DeleteWorkspaceMemberRehomeTest.php | 44 ++++++++++++++++-- .../Actions/Workspace/DeleteWorkspaceTest.php | 4 +- .../Billing/WorkspaceQuantitySyncTest.php | 2 +- tests/Feature/WorkspaceControllerTest.php | 33 +++++++++++-- tests/Unit/Policies/WorkspacePolicyTest.php | 12 ++--- 27 files changed, 201 insertions(+), 98 deletions(-) diff --git a/app/Actions/User/EnsurePersonalAccount.php b/app/Actions/User/EnsurePersonalAccount.php index 2862afc36..5292cd211 100644 --- a/app/Actions/User/EnsurePersonalAccount.php +++ b/app/Actions/User/EnsurePersonalAccount.php @@ -40,6 +40,52 @@ public static function execute(User $user): Account $user->update(['account_id' => $personalAccount->id]); + $currentBelongsToPersonal = $user->current_workspace_id + && $user->workspaces() + ->where('workspaces.account_id', $personalAccount->id) + ->where('workspaces.id', $user->current_workspace_id) + ->exists(); + + if (! $currentBelongsToPersonal) { + $fallback = $user->workspaces() + ->where('workspaces.account_id', $personalAccount->id) + ->first(); + + $user->update(['current_workspace_id' => $fallback?->id]); + } + return $personalAccount; } + + /** + * Move members off a shared account onto a personal account they own. + * + * @param bool $onlyWithoutAccountWorkspaces When true, only members with no + * remaining memberships on this + * account's workspaces are rehomed + * (workspace delete). When false, + * every other member is rehomed + * (account delete). + */ + public static function rehomeAccountMembers( + Account $account, + ?string $exceptUserId = null, + bool $onlyWithoutAccountWorkspaces = false, + ): void { + User::query() + ->where('account_id', $account->id) + ->when( + $exceptUserId, + fn ($query) => $query->where('id', '!=', $exceptUserId), + ) + ->when( + $onlyWithoutAccountWorkspaces, + fn ($query) => $query->whereDoesntHave( + 'workspaces', + fn ($workspaces) => $workspaces->where('workspaces.account_id', $account->id), + ), + ) + ->get() + ->each(fn (User $member) => self::execute($member)); + } } diff --git a/app/Actions/Workspace/DeleteWorkspace.php b/app/Actions/Workspace/DeleteWorkspace.php index 739994e71..84fd33fc0 100644 --- a/app/Actions/Workspace/DeleteWorkspace.php +++ b/app/Actions/Workspace/DeleteWorkspace.php @@ -9,34 +9,37 @@ use App\Models\User; use App\Models\Workspace; use App\Services\PostHogService; +use Illuminate\Support\Facades\DB; class DeleteWorkspace { - public static function execute(User $user, Workspace $workspace): void + public static function execute(Workspace $workspace): void { - User::where('current_workspace_id', $workspace->id) - ->get() - ->each(function (User $affected) use ($workspace): void { - $fallback = $affected->workspaces() - ->where('workspaces.id', '!=', $workspace->id) - ->first(); - - $affected->update(['current_workspace_id' => $fallback?->id]); - }); - $account = $workspace->account; $accountId = (string) $workspace->account_id; - $workspace->delete(); - - if ($account) { - User::query() - ->where('account_id', $account->id) - ->where('id', '!=', $account->owner_id) - ->whereDoesntHave('workspaces') + DB::transaction(function () use ($workspace, $account): void { + User::where('current_workspace_id', $workspace->id) ->get() - ->each(fn (User $stranded) => EnsurePersonalAccount::execute($stranded)); - } + ->each(function (User $affected) use ($workspace): void { + $fallback = $affected->workspaces() + ->where('workspaces.id', '!=', $workspace->id) + ->where('workspaces.account_id', $workspace->account_id) + ->first(); + + $affected->update(['current_workspace_id' => $fallback?->id]); + }); + + $workspace->delete(); + + if ($account) { + EnsurePersonalAccount::rehomeAccountMembers( + $account, + $account->owner_id, + onlyWithoutAccountWorkspaces: true, + ); + } + }); $account?->syncWorkspaceQuantity(); diff --git a/app/Http/Controllers/App/Settings/ProfileController.php b/app/Http/Controllers/App/Settings/ProfileController.php index 8082b7d8d..ad8148631 100644 --- a/app/Http/Controllers/App/Settings/ProfileController.php +++ b/app/Http/Controllers/App/Settings/ProfileController.php @@ -9,7 +9,6 @@ use App\Http\Requests\App\Settings\ProfileDeleteRequest; use App\Http\Requests\App\Settings\ProfileUpdateRequest; use App\Models\Account; -use App\Models\User; use App\Models\Workspace; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Http\RedirectResponse; @@ -89,11 +88,15 @@ public function destroy(ProfileDeleteRequest $request): RedirectResponse { $user = $request->user(); - DB::transaction(function () use ($user) { - $user->update(['current_workspace_id' => null]); + $account = $user->account; + $isOwner = $user->isAccountOwner(); + + if ($account && $isOwner && $account->subscribed(Account::SUBSCRIPTION_NAME)) { + $account->subscription(Account::SUBSCRIPTION_NAME)->cancelNow(); + } - $account = $user->account; - $isOwner = $user->isAccountOwner(); + DB::transaction(function () use ($user, $account, $isOwner) { + $user->update(['current_workspace_id' => null]); $ownedWorkspaces = Workspace::where('user_id', $user->id)->get(); @@ -102,7 +105,12 @@ public function destroy(ProfileDeleteRequest $request): RedirectResponse if ($member->id !== $user->id && $member->current_workspace_id === $workspace->id) { $otherWorkspace = $member->workspaces() ->where('workspaces.id', '!=', $workspace->id) - ->first(); + ->where('workspaces.account_id', $workspace->account_id) + ->first() + ?? $member->workspaces() + ->where('workspaces.id', '!=', $workspace->id) + ->first(); + $member->update(['current_workspace_id' => $otherWorkspace?->id]); } } @@ -118,15 +126,7 @@ public function destroy(ProfileDeleteRequest $request): RedirectResponse $user->workspaces()->detach(); if ($account && $isOwner) { - User::query() - ->where('account_id', $account->id) - ->where('id', '!=', $user->id) - ->get() - ->each(fn (User $member) => EnsurePersonalAccount::execute($member)); - - if ($account->subscribed(Account::SUBSCRIPTION_NAME)) { - $account->subscription(Account::SUBSCRIPTION_NAME)->cancelNow(); - } + EnsurePersonalAccount::rehomeAccountMembers($account, $user->id); $account->subscriptions()->delete(); $account->delete(); diff --git a/app/Http/Controllers/App/WorkspaceController.php b/app/Http/Controllers/App/WorkspaceController.php index ad2ee90dc..ad731ff00 100644 --- a/app/Http/Controllers/App/WorkspaceController.php +++ b/app/Http/Controllers/App/WorkspaceController.php @@ -160,6 +160,12 @@ public function settings(Request $request): Response|RedirectResponse && $workspace->account->workspaces()->count() <= 1, 'otherMemberCount' => $workspace->members() ->where('users.id', '!=', $user->id) + ->whereDoesntHave( + 'workspaces', + fn ($query) => $query + ->where('workspaces.account_id', $workspace->account_id) + ->where('workspaces.id', '!=', $workspace->id), + ) ->count(), ]); } @@ -238,17 +244,15 @@ public function updateSettings(UpdateWorkspaceRequest $request, LogoAttacher $lo return back()->with('flash.success', __('settings.flash.workspace_updated')); } - public function destroy(Request $request, Workspace $workspace): RedirectResponse + public function destroy(Workspace $workspace): RedirectResponse { $this->authorize('delete', $workspace); - $user = $request->user(); - if (! config('trypost.self_hosted') && $workspace->account->workspaces()->count() <= 1) { return back()->with('flash.error', __('workspaces.cannot_delete_last')); } - DeleteWorkspace::execute($user, $workspace); + DeleteWorkspace::execute($workspace); return redirect()->route('app.workspaces.index') ->with('flash.success', __('workspaces.flash.deleted')); diff --git a/app/Policies/WorkspacePolicy.php b/app/Policies/WorkspacePolicy.php index 028d37fdc..1bf5d51d7 100644 --- a/app/Policies/WorkspacePolicy.php +++ b/app/Policies/WorkspacePolicy.php @@ -32,17 +32,18 @@ public function update(User $user, Workspace $workspace): bool public function delete(User $user, Workspace $workspace): bool { - return $this->isOwnerOrWorkspaceAdmin($user, $workspace); + // Owner-only: deleting a workspace changes Stripe subscription quantity. + return $this->isOwner($user, $workspace); } public function restore(User $user, Workspace $workspace): bool { - return $this->isOwnerOrWorkspaceAdmin($user, $workspace); + return $this->isOwner($user, $workspace); } public function forceDelete(User $user, Workspace $workspace): bool { - return $this->isOwnerOrWorkspaceAdmin($user, $workspace); + return $this->isOwner($user, $workspace); } public function manageTeam(User $user, Workspace $workspace): bool diff --git a/lang/ar/settings.php b/lang/ar/settings.php index 03fdccfb1..3974a9565 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -140,7 +140,6 @@ 'name' => 'الاسم', 'name_placeholder' => 'مساحة عملي', 'save' => 'حفظ', - 'danger_title' => 'منطقة الخطر', 'danger_description' => 'إجراءات لا يمكن التراجع عنها.', 'delete_title' => 'حذف مساحة العمل هذه', 'delete_description' => 'يحذف مساحة العمل هذه وكل ما فيها نهائيًا — المنشورات والحسابات المتصلة والوسائط. ستُحاسب على مساحة عمل واحدة أقل.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'لا يمكنك حذف مساحة العمل الوحيدة لديك. ألغِ الاشتراك في الفوترة لإيقاف الدفع، أو احذف حسابك في المصادقة لإلغاء الفوترة وإزالة كل شيء نهائيًا.', 'delete_go_to_billing' => 'الانتقال إلى الفوترة', 'delete_go_to_delete_account' => 'حذف الحساب', - 'delete_members_warning' => '{1}سيفقد عضو آخر واحد الوصول.|[2,*]سيفقد :count أعضاء آخرين الوصول.', + 'delete_members_warning' => '{1}سيفقد عضو آخر واحد الوصول إلى هذا الحساب.|[2,*]سيفقد :count أعضاء آخرين الوصول إلى هذا الحساب.', 'delete_action' => 'حذف مساحة العمل', 'delete_cancel' => 'إلغاء', 'delete_confirm_title' => 'حذف مساحة العمل؟', diff --git a/lang/de/settings.php b/lang/de/settings.php index 75d430272..87b41ae7e 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -142,7 +142,6 @@ 'name' => 'Name', 'name_placeholder' => 'Mein Workspace', 'save' => 'Speichern', - 'danger_title' => 'Gefahrenzone', 'danger_description' => 'Unwiderrufliche Aktionen.', 'delete_title' => 'Diesen Workspace löschen', 'delete_description' => 'Löscht diesen Workspace dauerhaft — inklusive Beiträge, verbundener Konten und Medien. Du wirst für einen Workspace weniger berechnet.', @@ -150,7 +149,7 @@ 'delete_only_description' => 'Du kannst deinen einzigen Workspace nicht löschen. Kündige das Abonnement in der Abrechnung, um die Zahlung zu stoppen, oder lösche dein Konto unter Authentifizierung, um die Abrechnung zu beenden und alles dauerhaft zu entfernen.', 'delete_go_to_billing' => 'Zur Abrechnung', 'delete_go_to_delete_account' => 'Konto löschen', - 'delete_members_warning' => '{1}:count weiteres Mitglied verliert den Zugriff.|[2,*]:count weitere Mitglieder verlieren den Zugriff.', + 'delete_members_warning' => '{1}:count weiteres Mitglied verliert den Zugriff auf dieses Konto.|[2,*]:count weitere Mitglieder verlieren den Zugriff auf dieses Konto.', 'delete_action' => 'Workspace löschen', 'delete_cancel' => 'Abbrechen', 'delete_confirm_title' => 'Workspace löschen?', diff --git a/lang/el/settings.php b/lang/el/settings.php index 5bee4dc3d..6f032433f 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -140,7 +140,6 @@ 'name' => 'Όνομα', 'name_placeholder' => 'Το workspace μου', 'save' => 'Αποθήκευση', - 'danger_title' => 'Ζώνη κινδύνου', 'danger_description' => 'Μη αναστρέψιμες ενέργειες.', 'delete_title' => 'Διαγραφή αυτού του workspace', 'delete_description' => 'Διαγράφει οριστικά αυτό το workspace και ό,τι περιέχει — αναρτήσεις, συνδεδεμένους λογαριασμούς και μέσα. Θα χρεώνεστε για ένα λιγότερο workspace.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'Δεν μπορείτε να διαγράψετε το μοναδικό σας workspace. Ακυρώστε τη συνδρομή στη χρέωση για να σταματήσετε να πληρώνετε, ή διαγράψτε τον λογαριασμό σας στην Επαλήθευση για να ακυρώσετε τη χρέωση και να αφαιρέσετε τα πάντα οριστικά.', 'delete_go_to_billing' => 'Μετάβαση στη χρέωση', 'delete_go_to_delete_account' => 'Διαγραφή λογαριασμού', - 'delete_members_warning' => '{1}:count άλλο μέλος θα χάσει την πρόσβαση.|[2,*]:count άλλα μέλη θα χάσουν την πρόσβαση.', + 'delete_members_warning' => '{1}:count άλλο μέλος θα χάσει την πρόσβαση σε αυτόν τον λογαριασμό.|[2,*]:count άλλα μέλη θα χάσουν την πρόσβαση σε αυτόν τον λογαριασμό.', 'delete_action' => 'Διαγραφή workspace', 'delete_cancel' => 'Ακύρωση', 'delete_confirm_title' => 'Διαγραφή workspace;', diff --git a/lang/en/settings.php b/lang/en/settings.php index 646c2f49a..1989cbaa8 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -140,7 +140,6 @@ 'name' => 'Name', 'name_placeholder' => 'My Workspace', 'save' => 'Save', - 'danger_title' => 'Danger zone', 'danger_description' => 'Irreversible actions.', 'delete_title' => 'Delete this workspace', 'delete_description' => 'Permanently deletes this workspace and everything in it — posts, connected accounts, and media. You will be charged for one fewer workspace.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'You cannot delete your only workspace. Cancel your subscription in billing to stop paying, or delete your account in Authentication settings to cancel billing and permanently remove everything.', 'delete_go_to_billing' => 'Go to billing', 'delete_go_to_delete_account' => 'Delete account', - 'delete_members_warning' => '{1}:count other member will lose access.|[2,*]:count other members will lose access.', + 'delete_members_warning' => '{1}:count other member will lose access to this account.|[2,*]:count other members will lose access to this account.', 'delete_action' => 'Delete workspace', 'delete_cancel' => 'Cancel', 'delete_confirm_title' => 'Delete workspace?', diff --git a/lang/es/settings.php b/lang/es/settings.php index f2957caab..e8c381efd 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -140,7 +140,6 @@ 'name' => 'Nombre', 'name_placeholder' => 'Mi Workspace', 'save' => 'Guardar', - 'danger_title' => 'Zona de peligro', 'danger_description' => 'Acciones irreversibles.', 'delete_title' => 'Eliminar este workspace', 'delete_description' => 'Elimina este workspace de forma permanente, incluyendo publicaciones, cuentas conectadas y medios. Pasarás a pagar por un workspace menos.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'No puedes eliminar tu único workspace. Cancela la suscripción en facturación para dejar de pagar, o elimina tu cuenta en Autenticación para cancelar la facturación y borrar todo de forma permanente.', 'delete_go_to_billing' => 'Ir a facturación', 'delete_go_to_delete_account' => 'Eliminar cuenta', - 'delete_members_warning' => '{1}:count miembro más perderá el acceso.|[2,*]:count miembros más perderán el acceso.', + 'delete_members_warning' => '{1}:count miembro más perderá el acceso a esta cuenta.|[2,*]:count miembros más perderán el acceso a esta cuenta.', 'delete_action' => 'Eliminar workspace', 'delete_cancel' => 'Cancelar', 'delete_confirm_title' => '¿Eliminar workspace?', diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 703348974..2be202917 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -140,7 +140,6 @@ 'name' => 'Nom', 'name_placeholder' => 'Mon espace de travail', 'save' => 'Enregistrer', - 'danger_title' => 'Zone de danger', 'danger_description' => 'Actions irréversibles.', 'delete_title' => 'Supprimer cet espace de travail', 'delete_description' => 'Supprime définitivement cet espace de travail et tout son contenu — publications, comptes connectés et médias. Vous serez facturé pour un espace de travail de moins.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'Vous ne pouvez pas supprimer votre seul espace de travail. Annulez l’abonnement dans la facturation pour arrêter de payer, ou supprimez votre compte dans Authentification pour annuler la facturation et tout supprimer définitivement.', 'delete_go_to_billing' => 'Aller à la facturation', 'delete_go_to_delete_account' => 'Supprimer le compte', - 'delete_members_warning' => '{1}:count autre membre perdra l’accès.|[2,*]:count autres membres perdront l’accès.', + 'delete_members_warning' => '{1}:count autre membre perdra l’accès à ce compte.|[2,*]:count autres membres perdront l’accès à ce compte.', 'delete_action' => 'Supprimer l’espace de travail', 'delete_cancel' => 'Annuler', 'delete_confirm_title' => 'Supprimer l’espace de travail ?', diff --git a/lang/it/settings.php b/lang/it/settings.php index 6f4aa3413..706975501 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -140,7 +140,6 @@ 'name' => 'Nome', 'name_placeholder' => 'Il mio workspace', 'save' => 'Salva', - 'danger_title' => 'Zona pericolosa', 'danger_description' => 'Azioni irreversibili.', 'delete_title' => 'Elimina questo workspace', 'delete_description' => 'Elimina definitivamente questo workspace e tutto il suo contenuto — post, account collegati e media. Verrai addebitato per un workspace in meno.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'Non puoi eliminare il tuo unico workspace. Annulla l’abbonamento nella fatturazione per smettere di pagare, oppure elimina il tuo account in Autenticazione per annullare la fatturazione e rimuovere tutto in modo permanente.', 'delete_go_to_billing' => 'Vai alla fatturazione', 'delete_go_to_delete_account' => 'Elimina account', - 'delete_members_warning' => '{1}:count altro membro perderà l’accesso.|[2,*]:count altri membri perderanno l’accesso.', + 'delete_members_warning' => '{1}:count altro membro perderà l’accesso a questo account.|[2,*]:count altri membri perderanno l’accesso a questo account.', 'delete_action' => 'Elimina workspace', 'delete_cancel' => 'Annulla', 'delete_confirm_title' => 'Eliminare il workspace?', diff --git a/lang/ja/settings.php b/lang/ja/settings.php index 35180d91b..400a50ed2 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -140,7 +140,6 @@ 'name' => '名前', 'name_placeholder' => 'マイワークスペース', 'save' => '保存', - 'danger_title' => '危険な操作', 'danger_description' => '元に戻せない操作です。', 'delete_title' => 'このワークスペースを削除', 'delete_description' => 'このワークスペースと、投稿・連携アカウント・メディアを含むすべての内容を完全に削除します。請求はワークスペースが1つ減った分になります。', @@ -148,7 +147,7 @@ 'delete_only_description' => '唯一のワークスペースは削除できません。支払いを止めるには請求設定でサブスクリプションを解約するか、認証設定でアカウントを削除して請求を止め、すべてを完全に削除してください。', 'delete_go_to_billing' => '請求へ移動', 'delete_go_to_delete_account' => 'アカウントを削除', - 'delete_members_warning' => '{1}他のメンバー :count 人がアクセスできなくなります。|[2,*]他のメンバー :count 人がアクセスできなくなります。', + 'delete_members_warning' => '{1}他のメンバー :count 人がこのアカウントにアクセスできなくなります。|[2,*]他のメンバー :count 人がこのアカウントにアクセスできなくなります。', 'delete_action' => 'ワークスペースを削除', 'delete_cancel' => 'キャンセル', 'delete_confirm_title' => 'ワークスペースを削除しますか?', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 551ce44e0..7d6e09575 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -140,7 +140,6 @@ 'name' => '이름', 'name_placeholder' => '내 워크스페이스', 'save' => '저장', - 'danger_title' => '위험 구역', 'danger_description' => '되돌릴 수 없는 작업입니다.', 'delete_title' => '이 워크스페이스 삭제', 'delete_description' => '이 워크스페이스와 그 안의 모든 것(게시물, 연결된 계정, 미디어)을 영구 삭제합니다. 워크스페이스 하나만큼 요금이 줄어듭니다.', @@ -148,7 +147,7 @@ 'delete_only_description' => '유일한 워크스페이스는 삭제할 수 없습니다. 결제를 멈추려면 청구에서 구독을 취소하거나, 인증 설정에서 계정을 삭제해 결제를 취소하고 모든 데이터를 영구 삭제하세요.', 'delete_go_to_billing' => '결제로 이동', 'delete_go_to_delete_account' => '계정 삭제', - 'delete_members_warning' => '{1}다른 멤버 :count명이 접근 권한을 잃습니다.|[2,*]다른 멤버 :count명이 접근 권한을 잃습니다.', + 'delete_members_warning' => '{1}다른 멤버 :count명이 이 계정 접근 권한을 잃습니다.|[2,*]다른 멤버 :count명이 이 계정 접근 권한을 잃습니다.', 'delete_action' => '워크스페이스 삭제', 'delete_cancel' => '취소', 'delete_confirm_title' => '워크스페이스를 삭제할까요?', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index 886e8aa89..4566b7363 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -140,7 +140,6 @@ 'name' => 'Naam', 'name_placeholder' => 'Mijn workspace', 'save' => 'Opslaan', - 'danger_title' => 'Gevarenzone', 'danger_description' => 'Onomkeerbare acties.', 'delete_title' => 'Deze workspace verwijderen', 'delete_description' => 'Verwijdert deze workspace permanent, inclusief posts, gekoppelde accounts en media. Je wordt voor één workspace minder gefactureerd.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'Je kunt je enige workspace niet verwijderen. Zeg je abonnement op in facturatie om te stoppen met betalen, of verwijder je account bij Authenticatie om de facturatie te annuleren en alles permanent te wissen.', 'delete_go_to_billing' => 'Naar facturatie', 'delete_go_to_delete_account' => 'Account verwijderen', - 'delete_members_warning' => '{1}:count ander lid verliest toegang.|[2,*]:count andere leden verliezen toegang.', + 'delete_members_warning' => '{1}:count ander lid verliest toegang tot dit account.|[2,*]:count andere leden verliezen toegang tot dit account.', 'delete_action' => 'Workspace verwijderen', 'delete_cancel' => 'Annuleren', 'delete_confirm_title' => 'Workspace verwijderen?', diff --git a/lang/pl/settings.php b/lang/pl/settings.php index b68507d0c..36047ab71 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -140,7 +140,6 @@ 'name' => 'Nazwa', 'name_placeholder' => 'Moja przestrzeń robocza', 'save' => 'Zapisz', - 'danger_title' => 'Strefa zagrożenia', 'danger_description' => 'Nieodwracalne działania.', 'delete_title' => 'Usuń ten workspace', 'delete_description' => 'Trwale usuwa ten workspace wraz z postami, połączonymi kontami i mediami. Będziesz rozliczany za o jeden workspace mniej.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'Nie możesz usunąć swojego jedynego workspace. Anuluj subskrypcję w rozliczeniach, aby przestać płacić, albo usuń konto w Uwierzytelnianiu, aby anulować rozliczenia i trwale usunąć wszystko.', 'delete_go_to_billing' => 'Przejdź do rozliczeń', 'delete_go_to_delete_account' => 'Usuń konto', - 'delete_members_warning' => '{1}:count inny członek straci dostęp.|[2,*]:count innych członków straci dostęp.', + 'delete_members_warning' => '{1}:count inny członek straci dostęp do tego konta.|[2,*]:count innych członków straci dostęp do tego konta.', 'delete_action' => 'Usuń workspace', 'delete_cancel' => 'Anuluj', 'delete_confirm_title' => 'Usunąć workspace?', diff --git a/lang/pt-BR/settings.php b/lang/pt-BR/settings.php index 54b0b9d68..fab1fbf05 100644 --- a/lang/pt-BR/settings.php +++ b/lang/pt-BR/settings.php @@ -140,7 +140,6 @@ 'name' => 'Nome', 'name_placeholder' => 'Meu Workspace', 'save' => 'Salvar', - 'danger_title' => 'Zona de perigo', 'danger_description' => 'Ações irreversíveis.', 'delete_title' => 'Excluir este workspace', 'delete_description' => 'Exclui este workspace de forma permanente, incluindo posts, contas conectadas e mídia. Você passa a ser cobrado por um workspace a menos.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'Você não pode excluir seu único workspace. Cancele a assinatura na cobrança para parar de pagar, ou exclua sua conta em Autenticação para cancelar a cobrança e remover tudo permanentemente.', 'delete_go_to_billing' => 'Ir para cobrança', 'delete_go_to_delete_account' => 'Excluir conta', - 'delete_members_warning' => '{1}:count outro membro perderá o acesso.|[2,*]:count outros membros perderão o acesso.', + 'delete_members_warning' => '{1}:count outro membro perderá o acesso a esta conta.|[2,*]:count outros membros perderão o acesso a esta conta.', 'delete_action' => 'Excluir workspace', 'delete_cancel' => 'Cancelar', 'delete_confirm_title' => 'Excluir workspace?', diff --git a/lang/ru/settings.php b/lang/ru/settings.php index 6f3e3d8ea..b48e0b36c 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -140,7 +140,6 @@ 'name' => 'Название', 'name_placeholder' => 'Моё рабочее пространство', 'save' => 'Сохранить', - 'danger_title' => 'Опасная зона', 'danger_description' => 'Необратимые действия.', 'delete_title' => 'Удалить этот workspace', 'delete_description' => 'Безвозвратно удаляет этот workspace и всё в нём — посты, подключённые аккаунты и медиа. Вам будут выставлять счёт на один workspace меньше.', @@ -148,7 +147,7 @@ 'delete_only_description' => 'Вы не можете удалить свой единственный workspace. Отмените подписку в разделе оплаты, чтобы перестать платить, или удалите аккаунт в Аутентификации, чтобы отменить оплату и навсегда удалить всё.', 'delete_go_to_billing' => 'Перейти к оплате', 'delete_go_to_delete_account' => 'Удалить аккаунт', - 'delete_members_warning' => '{1}:count другой участник потеряет доступ.|[2,*]:count других участников потеряют доступ.', + 'delete_members_warning' => '{1}:count другой участник потеряет доступ к этому аккаунту.|[2,*]:count других участников потеряют доступ к этому аккаунту.', 'delete_action' => 'Удалить workspace', 'delete_cancel' => 'Отмена', 'delete_confirm_title' => 'Удалить workspace?', diff --git a/lang/tr/settings.php b/lang/tr/settings.php index ece214c68..8884a8016 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -142,7 +142,6 @@ 'name' => 'Ad', 'name_placeholder' => 'Çalışma Alanım', 'save' => 'Kaydet', - 'danger_title' => 'Tehlikeli bölge', 'danger_description' => 'Geri alınamaz işlemler.', 'delete_title' => 'Bu workspace’i sil', 'delete_description' => 'Bu workspace’i kalıcı olarak siler — gönderiler, bağlı hesaplar ve medya dahil. Bir workspace daha az ücretlendirilirsiniz.', @@ -150,7 +149,7 @@ 'delete_only_description' => 'Tek workspace’inizi silemezsiniz. Ödemeyi durdurmak için faturalandırmadan aboneliği iptal edin veya her şeyi kalıcı olarak silmek için Kimlik doğrulama ayarlarından hesabınızı silin.', 'delete_go_to_billing' => 'Faturalandırmaya git', 'delete_go_to_delete_account' => 'Hesabı sil', - 'delete_members_warning' => '{1}:count diğer üye erişimi kaybedecek.|[2,*]:count diğer üye erişimi kaybedecek.', + 'delete_members_warning' => '{1}:count diğer üye bu hesaba erişimi kaybedecek.|[2,*]:count diğer üye bu hesaba erişimi kaybedecek.', 'delete_action' => 'Workspace’i sil', 'delete_cancel' => 'İptal', 'delete_confirm_title' => 'Workspace silinsin mi?', diff --git a/lang/zh/settings.php b/lang/zh/settings.php index fb4a3d051..14a45ed0b 100644 --- a/lang/zh/settings.php +++ b/lang/zh/settings.php @@ -140,7 +140,6 @@ 'name' => '名称', 'name_placeholder' => '我的工作区', 'save' => '保存', - 'danger_title' => '危险区域', 'danger_description' => '不可撤销的操作。', 'delete_title' => '删除此工作区', 'delete_description' => '永久删除此工作区及其全部内容(帖子、已连接账户和媒体)。之后将按少一个工作区计费。', @@ -148,7 +147,7 @@ 'delete_only_description' => '无法删除唯一的工作区。可在账单中取消订阅以停止付费,或在身份验证设置中删除账户以取消计费并永久删除全部数据。', 'delete_go_to_billing' => '前往账单', 'delete_go_to_delete_account' => '删除账户', - 'delete_members_warning' => '{1}另有 :count 位成员将失去访问权限。|[2,*]另有 :count 位成员将失去访问权限。', + 'delete_members_warning' => '{1}另有 :count 位成员将失去对此账户的访问权限。|[2,*]另有 :count 位成员将失去对此账户的访问权限。', 'delete_action' => '删除工作区', 'delete_cancel' => '取消', 'delete_confirm_title' => '删除工作区?', diff --git a/resources/js/components/settings/DeleteWorkspace.vue b/resources/js/components/settings/DeleteWorkspace.vue index 253b9e4c8..94080c163 100644 --- a/resources/js/components/settings/DeleteWorkspace.vue +++ b/resources/js/components/settings/DeleteWorkspace.vue @@ -6,7 +6,6 @@ import { computed, ref } from 'vue'; import ConfirmDeleteModal from '@/components/ConfirmDeleteModal.vue'; import HeadingSmall from '@/components/HeadingSmall.vue'; import { Button } from '@/components/ui/button'; -import { useWorkspaceRole } from '@/composables/useWorkspaceRole'; import { edit as editAuthentication } from '@/routes/app/authentication'; import { index as billingIndex } from '@/routes/app/billing'; import { destroy as destroyWorkspace } from '@/routes/app/workspaces'; @@ -22,7 +21,6 @@ const props = defineProps<{ const page = usePage(); const isSelfHosted = computed(() => Boolean(page.props.selfHosted)); -const { canManageBilling } = useWorkspaceRole(); const deleteModal = ref | null>(null); @@ -90,7 +88,7 @@ const openDeleteModal = () => { > {{ $t('settings.workspace.delete_action') }} -