diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -3,6 +3,7 @@ namespace App; use App\Wallet; +use App\Traits\DomainConfigTrait; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -13,6 +14,7 @@ */ class Domain extends Model { + use DomainConfigTrait; use SoftDeletes; // we've simply never heard of this domain 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 @@ -96,6 +96,35 @@ return $this->errorResponse(404); } + /** + * Set the domain configuration. + * + * @param int $id Domain identifier + * + * @return \Illuminate\Http\JsonResponse|void + */ + public function setConfig($id) + { + $domain = Domain::findOrFail($id); + + // Only owner (or admin) has access to the domain + if (!Auth::guard()->user()->canRead($domain)) { + return $this->errorResponse(403); + } + + $errors = $domain->setConfig(request()->input()); + + if (!empty($errors)) { + return response()->json(['status' => 'error', 'errors' => $errors], 422); + } + + return response()->json([ + 'status' => 'success', + 'message' => \trans('app.domain-setconfig-success'), + ]); + } + + /** * Store a newly created resource in storage. * @@ -133,7 +162,10 @@ // Add DNS/MX configuration for the domain $response['dns'] = self::getDNSConfig($domain); - $response['config'] = self::getMXConfig($domain->namespace); + $response['mx'] = self::getMXConfig($domain->namespace); + + // Domain configuration, e.g. spf whitelist + $response['config'] = $domain->getConfig(); // Status info $response['statusInfo'] = self::statusInfo($domain); diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -77,6 +77,37 @@ return response()->json($result); } + /** + * Set user config. + * + * @param int $id The user + * + * @return \Illuminate\Http\JsonResponse + */ + public function setConfig($id) + { + $user = User::find($id); + + if (empty($user)) { + return $this->errorResponse(404); + } + + if (!$this->guard()->user()->canRead($user)) { + return $this->errorResponse(403); + } + + $errors = $user->setConfig(request()->input()); + + if (!empty($errors)) { + return response()->json(['status' => 'error', 'errors' => $errors], 422); + } + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-setconfig-success'), + ]); + } + /** * Display information on the user account specified by $id. * @@ -109,6 +140,8 @@ ]; } + $response['config'] = $user->getConfig(); + return response()->json($response); } diff --git a/src/app/Traits/DomainConfigTrait.php b/src/app/Traits/DomainConfigTrait.php new file mode 100644 --- /dev/null +++ b/src/app/Traits/DomainConfigTrait.php @@ -0,0 +1,57 @@ + $value) { + // validate and save SPF whitelist entries + if ($key === 'spf') { + if (!is_array($value)) { + $value = (array) $value; + } + + foreach ($value as $i => $v) { + if (empty($v)) { + unset($value[$i]); + continue; + } + + if ($v[0] !== '.' || !filter_var(substr($v, 1), FILTER_VALIDATE_DOMAIN)) { + $errors[$key][$i] = \trans('validation.spf-entry-invalid'); + } + } + + if (empty($errors[$key])) { + // TODO: Save the list to DB + } + } + } + + return $errors; + } +} diff --git a/src/app/Traits/UserConfigTrait.php b/src/app/Traits/UserConfigTrait.php new file mode 100644 --- /dev/null +++ b/src/app/Traits/UserConfigTrait.php @@ -0,0 +1,39 @@ + $value) { + if ($key == 'greylisting') { + // TODO: Save the value into the DB + } + } + + return $errors; + } +} diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -5,6 +5,7 @@ use App\Entitlement; use App\UserAlias; use App\Sku; +use App\Traits\UserConfigTrait; use App\Traits\UserAliasesTrait; use App\Traits\SettingsTrait; use App\Wallet; @@ -26,6 +27,7 @@ { use Notifiable; use NullableFields; + use UserConfigTrait; use UserAliasesTrait; use SettingsTrait; use SoftDeletes; 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 @@ -130,6 +130,10 @@ $('#app > .app-loader').addClass('fadeOut') this.isLoading = false }, + tab(e) { + e.preventDefault() + $(e.target).tab('show') + }, errorPage(code, msg) { // Until https://github.com/vuejs/vue-router/issues/977 is implemented // we can't really use router to display error page as it has two side 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 @@ -33,6 +33,7 @@ 'domain-verify-error' => 'Domain ownership verification failed.', 'domain-suspend-success' => 'Domain suspended successfully.', 'domain-unsuspend-success' => 'Domain unsuspended successfully.', + 'domain-setconfig-success' => 'Domain settings updated successfully.', 'user-update-success' => 'User data updated successfully.', 'user-create-success' => 'User created successfully.', @@ -40,6 +41,7 @@ 'user-suspend-success' => 'User suspended successfully.', 'user-unsuspend-success' => 'User unsuspended successfully.', 'user-reset-2fa-success' => '2-Factor authentication reset successfully.', + 'user-setconfig-success' => 'User settings updated successfully.', 'search-foundxdomains' => ':x domains have been found.', 'search-foundxusers' => ':x user accounts have been found.', diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php --- a/src/resources/lang/en/validation.php +++ b/src/resources/lang/en/validation.php @@ -134,6 +134,7 @@ 'entryexists' => 'The specified :attribute is not available.', 'minamount' => 'Minimum amount for a single payment is :amount.', 'minamountdebt' => 'The specified amount does not cover the balance on the account.', + 'spf-entry-invalid' => 'The entry format is invalid. Expected a domain name starting with a dot.', /* |-------------------------------------------------------------------------- diff --git a/src/resources/themes/app.scss b/src/resources/themes/app.scss --- a/src/resources/themes/app.scss +++ b/src/resources/themes/app.scss @@ -310,13 +310,8 @@ padding: 0.5rem 0; } - .form-group { - margin-bottom: 0.5rem; - } - .nav-tabs { flex-wrap: nowrap; - overflow-x: auto; .nav-link { white-space: nowrap; @@ -324,27 +319,6 @@ } } - .tab-content { - margin-top: 0.5rem; - } - - .col-form-label { - color: #666; - font-size: 95%; - } - - .form-group.plaintext .col-form-label { - padding-bottom: 0; - } - - form.read-only.short label { - width: 35%; - - & + * { - width: 65%; - } - } - #app > div.container { margin-bottom: 1rem; margin-top: 1rem; @@ -425,3 +399,9 @@ } } } + +@include media-breakpoint-down(sm) { + .tab-pane > .card-body { + padding: 0.5rem; + } +} diff --git a/src/resources/themes/forms.scss b/src/resources/themes/forms.scss --- a/src/resources/themes/forms.scss +++ b/src/resources/themes/forms.scss @@ -45,3 +45,48 @@ margin-bottom: 0; } } + +// Various improvements for mobile +@include media-breakpoint-down(sm) { + .form-group { + margin-bottom: 0.5rem; + } + + .form-group.plaintext .col-form-label { + padding-bottom: 0; + } + + form.read-only.short label { + width: 35%; + + & + * { + width: 65%; + } + } +} + +@include media-breakpoint-down(xs) { + .col-form-label { + color: #666; + font-size: 95%; + } + + .form-group.checkbox { + position: relative; + + & > div { + position: initial; + padding-top: 0 !important; + + input { + position: absolute; + top: 0.5rem; + right: 1rem; + } + } + + label { + padding-right: 2.5rem; + } + } +} diff --git a/src/resources/vue/Admin/Domain.vue b/src/resources/vue/Admin/Domain.vue --- a/src/resources/vue/Admin/Domain.vue +++ b/src/resources/vue/Admin/Domain.vue @@ -31,10 +31,15 @@
@@ -43,7 +48,23 @@

Domain DNS verification sample:

{{ domain.dns.join("\n") }}

Domain DNS configuration sample:

-

{{ domain.config.join("\n") }}

+

{{ domain.mx.join("\n") }}

+
+
+ +
+
+
+
+
+ +
+ + {{ domain.config && domain.config.spf.length ? domain.config.spf.join(', ') : 'none' }} + +
+
+
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue --- a/src/resources/vue/Admin/User.vue +++ b/src/resources/vue/Admin/User.vue @@ -108,6 +108,11 @@ Users ({{ users.length }}) +
@@ -259,6 +264,23 @@
+
+
+
+
+
+ +
+ + enabled + disabled + +
+
+
+
+
+