diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -381,6 +381,31 @@ } /** + * Checks if there are any objects (users/aliases/groups) in a domain. + * Note: Public domains are always reported not empty. + * + * @return bool True if there are no objects assigned, False otherwise + */ + public function isEmpty(): bool + { + if ($this->isPublic()) { + return false; + } + + // FIXME: These queries will not use indexes, so maybe we should consider + // wallet/entitlements to search in objects that belong to this domain account? + + $suffix = '@' . $this->namespace; + $suffixLen = strlen($suffix); + + return !( + \App\User::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() + || \App\UserAlias::whereRaw('substr(alias, ?) = ?', [-$suffixLen, $suffix])->exists() + || \App\Group::whereRaw('substr(email, ?) = ?', [-$suffixLen, $suffix])->exists() + ); + } + + /** * Any (additional) properties of this domain. * * @return \Illuminate\Database\Eloquent\Relations\HasMany @@ -436,8 +461,9 @@ /** * List the users of a domain, so long as the domain is not a public registration domain. + * Note: It returns only users with a mailbox. * - * @return array + * @return \App\User[] A list of users */ public function users(): array { diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php --- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php @@ -9,6 +9,18 @@ class DomainsController extends \App\Http\Controllers\API\V4\DomainsController { /** + * Remove the specified domain. + * + * @param int $id Domain identifier + * + * @return \Illuminate\Http\JsonResponse + */ + public function destroy($id) + { + return $this->errorResponse(404); + } + + /** * Search for domains * * @return \Illuminate\Http\JsonResponse diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php --- a/src/app/Http/Controllers/API/V4/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/DomainsController.php @@ -39,7 +39,7 @@ } /** - * Show the form for creating a new resource. + * Show the form for creating a new domain. * * @return \Illuminate\Http\JsonResponse */ @@ -82,21 +82,42 @@ } /** - * Remove the specified resource from storage. + * Remove the specified domain. * - * @param int $id + * @param int $id Domain identifier * * @return \Illuminate\Http\JsonResponse */ public function destroy($id) { - return $this->errorResponse(404); + $domain = Domain::withEnvTenantContext()->find($id); + + if (empty($domain)) { + return $this->errorResponse(404); + } + + if (!$this->guard()->user()->canDelete($domain)) { + return $this->errorResponse(403); + } + + // It is possible to delete domain only if there are no users/aliases/groups using it. + if (!$domain->isEmpty()) { + $response = ['status' => 'error', 'message' => \trans('app.domain-notempty-error')]; + return response()->json($response, 422); + } + + $domain->delete(); + + return response()->json([ + 'status' => 'success', + 'message' => \trans('app.domain-delete-success'), + ]); } /** - * Show the form for editing the specified resource. + * Show the form for editing the specified domain. * - * @param int $id + * @param int $id Domain identifier * * @return \Illuminate\Http\JsonResponse */ @@ -168,9 +189,15 @@ $namespace = \strtolower(request()->input('namespace')); // Domain already exists - if (Domain::withTrashed()->where('namespace', $namespace)->exists()) { - $errors = ['namespace' => \trans('validation.domainnotavailable')]; - return response()->json(['status' => 'error', 'errors' => $errors], 422); + if ($domain = Domain::withTrashed()->where('namespace', $namespace)->first()) { + // Check if the domain is soft-deleted and belongs to the same user + $deleteBeforeCreate = $domain->trashed() && ($wallet = $domain->wallet()) + && $wallet->owner && $wallet->owner->id == $owner->id; + + if (!$deleteBeforeCreate) { + $errors = ['namespace' => \trans('validation.domainnotavailable')]; + return response()->json(['status' => 'error', 'errors' => $errors], 422); + } } if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) { @@ -185,7 +212,10 @@ DB::beginTransaction(); - // TODO: Force-delete domain if it is soft-deleted and belongs to the same user + // Force-delete the existing domain if it is soft-deleted and belongs to the same user + if (!empty($deleteBeforeCreate)) { + $domain->forceDelete(); + } // Create the domain $domain = Domain::create([ @@ -309,10 +339,10 @@ } /** - * Update the specified resource in storage. + * Update the specified domain. * * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $id Domain identifier * * @return \Illuminate\Http\JsonResponse */ diff --git a/src/resources/js/app.js b/src/resources/js/app.js --- a/src/resources/js/app.js +++ b/src/resources/js/app.js @@ -452,9 +452,6 @@ return response }, error => { - let error_msg - let status = error.response ? error.response.status : 200 - // Do not display the error in a toast message, pass the error as-is if (error.config.ignoreErrors) { return Promise.reject(error) @@ -464,15 +461,20 @@ error.config.onFinish() } - if (error.response && status == 422) { - error_msg = "Form validation error" + let error_msg + + const status = error.response ? error.response.status : 200 + const data = error.response ? error.response.data : {} + + if (status == 422 && data.errors) { + error_msg = app.$t('error.form') const modal = $('div.modal.show') $(modal.length ? modal : 'form').each((i, form) => { form = $(form) - $.each(error.response.data.errors || {}, (idx, msg) => { + $.each(data.errors, (idx, msg) => { const input_name = (form.data('validation-prefix') || form.find('form').first().data('validation-prefix') || '') + idx let input = form.find('#' + input_name) @@ -526,8 +528,8 @@ form.find('.is-invalid:not(.listinput-widget)').first().focus() }) } - else if (error.response && error.response.data) { - error_msg = error.response.data.message + else if (data.status == 'error') { + error_msg = data.message } else { error_msg = error.request ? error.request.statusText : error.message diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php --- a/src/resources/lang/en/app.php +++ b/src/resources/lang/en/app.php @@ -51,6 +51,8 @@ 'distlist-unsuspend-success' => 'Distribution list unsuspended successfully.', 'domain-create-success' => 'Domain created successfully.', + 'domain-delete-success' => 'Domain deleted successfully.', + 'domain-notempty-error' => 'Unable to delete a domain with assigned users or other objects.', 'domain-verify-success' => 'Domain verified successfully.', 'domain-verify-error' => 'Domain ownership verification failed.', 'domain-suspend-success' => 'Domain suspended successfully.', diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php --- a/src/resources/lang/en/ui.php +++ b/src/resources/lang/en/ui.php @@ -61,6 +61,11 @@ ], 'domain' => [ + 'delete' => "Delete domain", + 'delete-domain' => "Delete {domain}", + 'delete-text' => "Do you really want to delete this domain permanently?" + . " This is only possible if there are no users, aliases or other objects in this domain." + . " Please note that this action cannot be undone.", 'dns-verify' => "Domain DNS verification sample:", 'dns-config' => "Domain DNS configuration sample:", 'namespace' => "Namespace", @@ -92,6 +97,7 @@ '500' => "Internal server error", 'unknown' => "Unknown Error", 'server' => "Server Error", + 'form' => "Form validation error", ], 'form' => [ diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue --- a/src/resources/vue/Domain/Info.vue +++ b/src/resources/vue/Domain/Info.vue @@ -5,7 +5,14 @@
{{ $t('domain.new') }}
-
{{ $t('form.domain') }}
+
{{ $t('form.domain') }} + +
+