Page MenuHomePhorge

D1870.1775206022.diff
No OneTemporary

Authored By
Unknown
Size
51 KB
Referenced Files
None
Subscribers
None

D1870.1775206022.diff

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 @@
+<?php
+
+namespace App\Traits;
+
+trait DomainConfigTrait
+{
+ /**
+ * A helper to get the domain configuration.
+ */
+ public function getConfig(): array
+ {
+ $config = [];
+
+ // TODO: Get the list from DB
+ $config['spf'] = [];
+
+ return $config;
+ }
+
+ /**
+ * A helper to update domain configuration.
+ *
+ * @param array $config An array of configuration options
+ *
+ * @return array A list of input validation errors
+ */
+ public function setConfig(array $config): array
+ {
+ $errors = [];
+
+ foreach ($config as $key => $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 @@
+<?php
+
+namespace App\Traits;
+
+trait UserConfigTrait
+{
+ /**
+ * A helper to get the user configuration.
+ */
+ public function getConfig(): array
+ {
+ $config = [];
+
+ // TODO: Get the value from DB
+ $config['greylisting'] = true;
+
+ return $config;
+ }
+
+ /**
+ * A helper to update user configuration.
+ *
+ * @param array $config An array of configuration options
+ *
+ * @return array A list of input validation error messages
+ */
+ public function setConfig(array $config): array
+ {
+ $errors = [];
+
+ foreach ($config as $key => $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 @@
</div>
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
- <a class="nav-link active" id="tab-config" href="#domain-config" role="tab" aria-controls="domain-config" aria-selected="true">
+ <a class="nav-link active" id="tab-config" href="#domain-config" role="tab" aria-controls="domain-config" aria-selected="true" @click="$root.tab">
Configuration
</a>
</li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-settings" href="#domain-settings" role="tab" aria-controls="domain-settings" aria-selected="false" @click="$root.tab">
+ Settings
+ </a>
+ </li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="domain-config" role="tabpanel" aria-labelledby="tab-config">
@@ -43,7 +48,23 @@
<p>Domain DNS verification sample:</p>
<p><pre id="dns-verify">{{ domain.dns.join("\n") }}</pre></p>
<p>Domain DNS configuration sample:</p>
- <p><pre id="dns-config">{{ domain.config.join("\n") }}</pre></p>
+ <p><pre id="dns-config">{{ domain.mx.join("\n") }}</pre></p>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="domain-settings" role="tabpanel" aria-labelledby="tab-settings">
+ <div class="card-body">
+ <div class="card-text">
+ <form class="read-only short">
+ <div class="form-group row plaintext">
+ <label for="spf" class="col-sm-4 col-form-label">SPF Whitelist</label>
+ <div class="col-sm-8">
+ <span class="form-control-plaintext" id="spf">
+ {{ domain.config && domain.config.spf.length ? domain.config.spf.join(', ') : 'none' }}
+ </span>
+ </div>
+ </div>
+ </form>
</div>
</div>
</div>
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 }})
</a>
</li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-settings" href="#user-settings" role="tab" aria-controls="user-settings" aria-selected="false">
+ Settings
+ </a>
+ </li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="user-finances" role="tabpanel" aria-labelledby="tab-finances">
@@ -259,6 +264,23 @@
</div>
</div>
</div>
+ <div class="tab-pane" id="user-settings" role="tabpanel" aria-labelledby="tab-settings">
+ <div class="card-body">
+ <div class="card-text">
+ <form class="read-only short">
+ <div class="form-group row plaintext">
+ <label for="greylisting" class="col-sm-4 col-form-label">Greylisting</label>
+ <div class="col-sm-8">
+ <span class="form-control-plaintext" id="greylisting">
+ <span v-if="user.config.greylisting" class="text-success">enabled</span>
+ <span v-else class="text-danger">disabled</span>
+ </span>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
</div>
<div id="discount-dialog" class="modal" tabindex="-1" role="dialog">
@@ -405,6 +427,7 @@
users: [],
user: {
aliases: [],
+ config: {},
wallet: {},
skus: {},
}
@@ -494,10 +517,7 @@
.catch(this.$root.errorHandler)
},
mounted() {
- $(this.$el).find('ul.nav-tabs a').on('click', e => {
- e.preventDefault()
- $(e.target).tab('show')
- })
+ $(this.$el).find('ul.nav-tabs a').on('click', this.$root.tab)
},
methods: {
capitalize(str) {
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
@@ -2,34 +2,76 @@
<div class="container">
<status-component :status="status" @status-update="statusUpdate"></status-component>
- <div v-if="domain && !domain.isConfirmed" class="card" id="domain-verify">
+ <div v-if="domain" class="card">
<div class="card-body">
- <div class="card-title">Domain verification</div>
+ <div class="card-title">{{ domain.namespace }}</div>
<div class="card-text">
- <p>In order to confirm that you're the actual holder of the domain,
- we need to run a verification process before finally activating it for email delivery.</p>
- <p>The domain <b>must have one of the following entries</b> in DNS:
- <ul>
- <li>TXT entry with value: <code>{{ domain.hash_text }}</code></li>
- <li>or CNAME entry: <code>{{ domain.hash_cname }}.{{ domain.namespace }}. IN CNAME {{ domain.hash_code }}.{{ domain.namespace }}.</code></li>
- </ul>
- When this is done press the button below to start the verification.</p>
- <p>Here's a sample zone file for your domain: <pre>{{ domain.dns.join("\n") }}</pre></p>
- <button class="btn btn-primary" type="button" @click="confirm"><svg-icon icon="sync-alt"></svg-icon> Verify</button>
- </div>
- </div>
- </div>
- <div v-if="domain && domain.isConfirmed" class="card" id="domain-config">
- <div class="card-body">
- <div class="card-title">Domain configuration</div>
- <div class="card-text">
- <p>In order to let {{ app_name }} receive email traffic for your domain you need to adjust
- the DNS settings, more precisely the MX entries, accordingly.</p>
- <p>Edit your domain's zone file and replace existing MX
- entries with the following values: <pre>{{ domain.config.join("\n") }}</pre></p>
- <p>If you don't know how to set DNS entries for your domain,
- please contact the registration service where you registered
- the domain or your web hosting provider.</p>
+ <ul class="nav nav-tabs mt-3" role="tablist">
+ <li class="nav-item" v-if="!domain.isConfirmed">
+ <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
+ Domain verification
+ </a>
+ </li>
+ <li class="nav-item" v-if="domain.isConfirmed">
+ <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
+ Domain configuration
+ </a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
+ Settings
+ </a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
+ <div v-if="!domain.isConfirmed" class="card-body" id="domain-verify">
+ <div class="card-text">
+ <p>In order to confirm that you're the actual holder of the domain,
+ we need to run a verification process before finally activating it for email delivery.</p>
+ <p>The domain <b>must have one of the following entries</b> in DNS:
+ <ul>
+ <li>TXT entry with value: <code>{{ domain.hash_text }}</code></li>
+ <li>or CNAME entry: <code>{{ domain.hash_cname }}.{{ domain.namespace }}. IN CNAME {{ domain.hash_code }}.{{ domain.namespace }}.</code></li>
+ </ul>
+ When this is done press the button below to start the verification.</p>
+ <p>Here's a sample zone file for your domain: <pre>{{ domain.dns.join("\n") }}</pre></p>
+ <button class="btn btn-primary" type="button" @click="confirm"><svg-icon icon="sync-alt"></svg-icon> Verify</button>
+ </div>
+ </div>
+ <div v-if="domain.isConfirmed" class="card-body" id="domain-config">
+ <div class="card-text">
+ <p>In order to let {{ app_name }} receive email traffic for your domain you need to adjust
+ the DNS settings, more precisely the MX entries, accordingly.</p>
+ <p>Edit your domain's zone file and replace existing MX
+ entries with the following values: <pre>{{ domain.mx.join("\n") }}</pre></p>
+ <p>If you don't know how to set DNS entries for your domain,
+ please contact the registration service where you registered
+ the domain or your web hosting provider.</p>
+ </div>
+ </div>
+ </div>
+ <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
+ <div class="card-body">
+ <form @submit.prevent="submitSettings">
+ <div class="form-group row">
+ <label for="spf" class="col-sm-4 col-form-label">SPF Whitelist</label>
+ <div class="col-sm-8">
+ <list-input id="spf" name="spf" :list="spf"></list-input>
+ <small id="spf-hint" class="form-text text-muted">
+ The Sender Policy Framework allows a sender domain to disclose, through DNS,
+ which systems are allowed to send emails with an envelope sender address within said domain.
+ <span class="d-block">
+ Here you can specify a list of allowed servers, for example: <var>.ess.barracuda.com</var>.
+ </span>
+ </small>
+ </div>
+ </div>
+ <button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Submit</button>
+ </form>
+ </div>
+ </div>
+ </div>
</div>
</div>
</div>
@@ -37,10 +79,12 @@
</template>
<script>
+ import ListInput from '../Widgets/ListInput'
import StatusComponent from '../Widgets/Status'
export default {
components: {
+ ListInput,
StatusComponent
},
data() {
@@ -48,6 +92,7 @@
domain_id: null,
domain: null,
app_name: window.config['app.name'],
+ spf: [],
status: {}
}
},
@@ -59,6 +104,7 @@
.then(response => {
this.$root.stopLoading()
this.domain = response.data
+ this.spf = this.domain.config.spf || []
if (!this.domain.isConfirmed) {
$('#domain-verify button').focus()
@@ -87,6 +133,16 @@
},
statusUpdate(domain) {
this.domain = Object.assign({}, this.domain, domain)
+ },
+ submitSettings() {
+ this.$root.clearFormValidation($('#settings form'))
+
+ let post = { spf: this.spf }
+
+ axios.post('/api/v4/domains/' + this.domain_id + '/config', post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ })
}
}
}
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -7,151 +7,187 @@
<div class="card-title" v-if="user_id !== 'new'">User account</div>
<div class="card-title" v-if="user_id === 'new'">New user account</div>
<div class="card-text">
- <form @submit.prevent="submit">
- <div v-if="user_id !== 'new'" class="form-group row plaintext">
- <label for="first_name" class="col-sm-4 col-form-label">Status</label>
- <div class="col-sm-8">
- <span :class="$root.userStatusClass(user) + ' form-control-plaintext'" id="status">{{ $root.userStatusText(user) }}</span>
+ <ul class="nav nav-tabs mt-3" role="tablist">
+ <li class="nav-item">
+ <a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
+ General
+ </a>
+ </li>
+ <li v-if="user_id !== 'new'" class="nav-item">
+ <a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
+ Settings
+ </a>
+ </li>
+ </ul>
+ <div class="tab-content">
+ <div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
+ <div class="card-body">
+ <form @submit.prevent="submit">
+ <div v-if="user_id !== 'new'" class="form-group row plaintext">
+ <label for="first_name" class="col-sm-4 col-form-label">Status</label>
+ <div class="col-sm-8">
+ <span :class="$root.userStatusClass(user) + ' form-control-plaintext'" id="status">{{ $root.userStatusText(user) }}</span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="first_name" class="col-sm-4 col-form-label">First name</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="first_name" v-model="user.first_name">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="last_name" class="col-sm-4 col-form-label">Last name</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="last_name" v-model="user.last_name">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="organization" class="col-sm-4 col-form-label">Organization</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="organization" v-model="user.organization">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="email" class="col-sm-4 col-form-label">Email</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="email" :disabled="user_id !== 'new'" required v-model="user.email">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="aliases-input" class="col-sm-4 col-form-label">Email aliases</label>
+ <div class="col-sm-8">
+ <list-input id="aliases" :list="user.aliases"></list-input>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="password" class="col-sm-4 col-form-label">Password</label>
+ <div class="col-sm-8">
+ <input type="password" class="form-control" id="password" v-model="user.password" :required="user_id === 'new'">
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="password_confirmaton" class="col-sm-4 col-form-label">Confirm password</label>
+ <div class="col-sm-8">
+ <input type="password" class="form-control" id="password_confirmation" v-model="user.password_confirmation" :required="user_id === 'new'">
+ </div>
+ </div>
+ <div v-if="user_id === 'new'" id="user-packages" class="form-group row">
+ <label class="col-sm-4 col-form-label">Package</label>
+ <div class="col-sm-8">
+ <table class="table table-sm form-list">
+ <thead class="thead-light sr-only">
+ <tr>
+ <th scope="col"></th>
+ <th scope="col">Package</th>
+ <th scope="col">Price</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="pkg in packages" :id="'p' + pkg.id" :key="pkg.id">
+ <td class="selection">
+ <input type="checkbox" @click="selectPackage"
+ :value="pkg.id"
+ :checked="pkg.id == package_id"
+ :id="'pkg-input-' + pkg.id"
+ >
+ </td>
+ <td class="name">
+ <label :for="'pkg-input-' + pkg.id">{{ pkg.name }}</label>
+ </td>
+ <td class="price text-nowrap">
+ {{ $root.priceLabel(pkg.cost, 1, discount) }}
+ </td>
+ <td class="buttons">
+ <button v-if="pkg.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip.click="pkg.description">
+ <svg-icon icon="info-circle"></svg-icon>
+ <span class="sr-only">More information</span>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <small v-if="discount > 0" class="hint">
+ <hr class="m-0">
+ &sup1; applied discount: {{ discount }}% - {{ discount_description }}
+ </small>
+ </div>
+ </div>
+ <div v-if="user_id !== 'new'" id="user-skus" class="form-group row">
+ <label class="col-sm-4 col-form-label">Subscriptions</label>
+ <div class="col-sm-8">
+ <table class="table table-sm form-list">
+ <thead class="thead-light sr-only">
+ <tr>
+ <th scope="col"></th>
+ <th scope="col">Subscription</th>
+ <th scope="col">Price</th>
+ <th scope="col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="sku in skus" :id="'s' + sku.id" :key="sku.id">
+ <td class="selection">
+ <input type="checkbox" @input="onInputSku"
+ :value="sku.id"
+ :disabled="sku.readonly"
+ :checked="sku.enabled"
+ :id="'sku-input-' + sku.title"
+ >
+ </td>
+ <td class="name">
+ <label :for="'sku-input-' + sku.title">{{ sku.name }}</label>
+ <div v-if="sku.range" class="range-input">
+ <label class="text-nowrap">{{ sku.range.min }} {{ sku.range.unit }}</label>
+ <input
+ type="range" class="custom-range" @input="rangeUpdate"
+ :value="sku.value || sku.range.min"
+ :min="sku.range.min"
+ :max="sku.range.max"
+ >
+ </div>
+ </td>
+ <td class="price text-nowrap">
+ {{ $root.priceLabel(sku.cost, 1, discount) }}
+ </td>
+ <td class="buttons">
+ <button v-if="sku.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip.click="sku.description">
+ <svg-icon icon="info-circle"></svg-icon>
+ <span class="sr-only">More information</span>
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <small v-if="discount > 0" class="hint">
+ <hr class="m-0">
+ &sup1; applied discount: {{ discount }}% - {{ discount_description }}
+ </small>
+ </div>
+ </div>
+ <button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Submit</button>
+ </form>
</div>
</div>
- <div class="form-group row">
- <label for="first_name" class="col-sm-4 col-form-label">First name</label>
- <div class="col-sm-8">
- <input type="text" class="form-control" id="first_name" v-model="user.first_name">
+ <div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
+ <div class="card-body">
+ <form @submit.prevent="submitSettings">
+ <div class="form-group row checkbox">
+ <label for="greylisting" class="col-sm-4 col-form-label">Greylisting</label>
+ <div class="col-sm-8 pt-2">
+ <input type="checkbox" id="greylisting" name="greylisting" value="1" :checked="user.config.greylisting">
+ <small id="greylisting-hint" class="form-text text-muted">
+ Greylisting is a method of defending users against spam. Any incoming mail from an unrecognized sender
+ is temporarily rejected. The originating server should try again after a delay.
+ This time the email will be accepted. Spammers usually do not reattempt mail delivery.
+ </small>
+ </div>
+ </div>
+ <button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Submit</button>
+ </form>
</div>
</div>
- <div class="form-group row">
- <label for="last_name" class="col-sm-4 col-form-label">Last name</label>
- <div class="col-sm-8">
- <input type="text" class="form-control" id="last_name" v-model="user.last_name">
- </div>
- </div>
- <div class="form-group row">
- <label for="organization" class="col-sm-4 col-form-label">Organization</label>
- <div class="col-sm-8">
- <input type="text" class="form-control" id="organization" v-model="user.organization">
- </div>
- </div>
- <div class="form-group row">
- <label for="email" class="col-sm-4 col-form-label">Email</label>
- <div class="col-sm-8">
- <input type="text" class="form-control" id="email" :disabled="user_id !== 'new'" required v-model="user.email">
- </div>
- </div>
- <div class="form-group row">
- <label for="aliases-input" class="col-sm-4 col-form-label">Email aliases</label>
- <div class="col-sm-8">
- <list-input id="aliases" :list="user.aliases"></list-input>
- </div>
- </div>
- <div class="form-group row">
- <label for="password" class="col-sm-4 col-form-label">Password</label>
- <div class="col-sm-8">
- <input type="password" class="form-control" id="password" v-model="user.password" :required="user_id === 'new'">
- </div>
- </div>
- <div class="form-group row">
- <label for="password_confirmaton" class="col-sm-4 col-form-label">Confirm password</label>
- <div class="col-sm-8">
- <input type="password" class="form-control" id="password_confirmation" v-model="user.password_confirmation" :required="user_id === 'new'">
- </div>
- </div>
- <div v-if="user_id === 'new'" id="user-packages" class="form-group row">
- <label class="col-sm-4 col-form-label">Package</label>
- <div class="col-sm-8">
- <table class="table table-sm form-list">
- <thead class="thead-light sr-only">
- <tr>
- <th scope="col"></th>
- <th scope="col">Package</th>
- <th scope="col">Price</th>
- <th scope="col"></th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="pkg in packages" :id="'p' + pkg.id" :key="pkg.id">
- <td class="selection">
- <input type="checkbox" @click="selectPackage"
- :value="pkg.id"
- :checked="pkg.id == package_id"
- :id="'pkg-input-' + pkg.id"
- >
- </td>
- <td class="name">
- <label :for="'pkg-input-' + pkg.id">{{ pkg.name }}</label>
- </td>
- <td class="price text-nowrap">
- {{ $root.priceLabel(pkg.cost, 1, discount) }}
- </td>
- <td class="buttons">
- <button v-if="pkg.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip.click="pkg.description">
- <svg-icon icon="info-circle"></svg-icon>
- <span class="sr-only">More information</span>
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- <small v-if="discount > 0" class="hint">
- <hr class="m-0">
- &sup1; applied discount: {{ discount }}% - {{ discount_description }}
- </small>
- </div>
- </div>
- <div v-if="user_id !== 'new'" id="user-skus" class="form-group row">
- <label class="col-sm-4 col-form-label">Subscriptions</label>
- <div class="col-sm-8">
- <table class="table table-sm form-list">
- <thead class="thead-light sr-only">
- <tr>
- <th scope="col"></th>
- <th scope="col">Subscription</th>
- <th scope="col">Price</th>
- <th scope="col"></th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="sku in skus" :id="'s' + sku.id" :key="sku.id">
- <td class="selection">
- <input type="checkbox" @input="onInputSku"
- :value="sku.id"
- :disabled="sku.readonly"
- :checked="sku.enabled"
- :id="'sku-input-' + sku.title"
- >
- </td>
- <td class="name">
- <label :for="'sku-input-' + sku.title">{{ sku.name }}</label>
- <div v-if="sku.range" class="range-input">
- <label class="text-nowrap">{{ sku.range.min }} {{ sku.range.unit }}</label>
- <input
- type="range" class="custom-range" @input="rangeUpdate"
- :value="sku.value || sku.range.min"
- :min="sku.range.min"
- :max="sku.range.max"
- >
- </div>
- </td>
- <td class="price text-nowrap">
- {{ $root.priceLabel(sku.cost, 1, discount) }}
- </td>
- <td class="buttons">
- <button v-if="sku.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip.click="sku.description">
- <svg-icon icon="info-circle"></svg-icon>
- <span class="sr-only">More information</span>
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- <small v-if="discount > 0" class="hint">
- <hr class="m-0">
- &sup1; applied discount: {{ discount }}% - {{ discount_description }}
- </small>
- </div>
- </div>
- <button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> Submit</button>
- </form>
+ </div>
</div>
</div>
</div>
@@ -172,7 +208,7 @@
discount: 0,
discount_description: '',
user_id: null,
- user: { aliases: [] },
+ user: { aliases: [], config: [] },
packages: [],
package_id: null,
skus: [],
@@ -280,6 +316,15 @@
this.$router.push({ name: 'users' })
})
},
+ submitSettings() {
+ this.$root.clearFormValidation($('#settings form'))
+ let post = { greylisting: $('#greylisting').prop('checked') ? 1 : 0 }
+
+ axios.post('/api/v4/users/' + this.user_id + '/config', post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ })
+ },
onInputSku(e) {
let input = e.target
let sku = this.findSku(input.value)
diff --git a/src/resources/vue/Widgets/ListInput.vue b/src/resources/vue/Widgets/ListInput.vue
--- a/src/resources/vue/Widgets/ListInput.vue
+++ b/src/resources/vue/Widgets/ListInput.vue
@@ -49,10 +49,13 @@
if (focus !== false) {
this.input.focus()
}
+
+ this.$emit('change', this.$el)
}
},
deleteItem(index) {
this.$delete(this.list, index)
+ this.$emit('change', this.$el)
if (this.list.length == 1) {
$(this.$el).removeClass('is-invalid')
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -62,11 +62,13 @@
Route::apiResource('domains', API\V4\DomainsController::class);
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
Route::get('domains/{id}/status', 'API\V4\DomainsController@status');
+ Route::post('domains/{id}/config', 'API\V4\DomainsController@setConfig');
Route::apiResource('entitlements', API\V4\EntitlementsController::class);
Route::apiResource('packages', API\V4\PackagesController::class);
Route::apiResource('skus', API\V4\SkusController::class);
Route::apiResource('users', API\V4\UsersController::class);
+ Route::post('users/{id}/config', 'API\V4\UsersController@setConfig');
Route::get('users/{id}/skus', 'API\V4\SkusController@userSkus');
Route::get('users/{id}/status', 'API\V4\UsersController@status');

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 8:47 AM (13 h, 27 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823310
Default Alt Text
D1870.1775206022.diff (51 KB)

Event Timeline