Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117845838
D1057.1775311307.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
23 KB
Referenced Files
None
Subscribers
None
D1057.1775311307.diff
View Options
diff --git a/src/app/Handlers/Groupware.php b/src/app/Handlers/Activesync.php
copy from src/app/Handlers/Groupware.php
copy to src/app/Handlers/Activesync.php
--- a/src/app/Handlers/Groupware.php
+++ b/src/app/Handlers/Activesync.php
@@ -2,14 +2,14 @@
namespace App\Handlers;
-class Groupware extends \App\Handlers\Base
+class Activesync extends \App\Handlers\Base
{
public static function entitleableClass()
{
return \App\User::class;
}
- public static function preReq($entitlement, $user)
+ public static function preReq($entitlement, $object)
{
if (!$entitlement->sku->active) {
\Log::error("Sku not active");
diff --git a/src/app/Handlers/Groupware.php b/src/app/Handlers/Auth2F.php
copy from src/app/Handlers/Groupware.php
copy to src/app/Handlers/Auth2F.php
--- a/src/app/Handlers/Groupware.php
+++ b/src/app/Handlers/Auth2F.php
@@ -2,14 +2,14 @@
namespace App\Handlers;
-class Groupware extends \App\Handlers\Base
+class Auth2F extends \App\Handlers\Base
{
public static function entitleableClass()
{
return \App\User::class;
}
- public static function preReq($entitlement, $user)
+ public static function preReq($entitlement, $object)
{
if (!$entitlement->sku->active) {
\Log::error("Sku not active");
diff --git a/src/app/Handlers/Base.php b/src/app/Handlers/Base.php
--- a/src/app/Handlers/Base.php
+++ b/src/app/Handlers/Base.php
@@ -26,4 +26,15 @@
{
//
}
+
+ /**
+ * The priority that specifies the order of SKUs in UI.
+ * Higher number means higher on the list.
+ *
+ * @return int
+ */
+ public static function priority(): int
+ {
+ return 0;
+ }
}
diff --git a/src/app/Handlers/Groupware.php b/src/app/Handlers/Groupware.php
--- a/src/app/Handlers/Groupware.php
+++ b/src/app/Handlers/Groupware.php
@@ -18,4 +18,15 @@
return true;
}
+
+ /**
+ * The priority that specifies the order of SKUs in UI.
+ * Higher number means higher on the list.
+ *
+ * @return int
+ */
+ public static function priority(): int
+ {
+ return 80;
+ }
}
diff --git a/src/app/Handlers/Mailbox.php b/src/app/Handlers/Mailbox.php
--- a/src/app/Handlers/Mailbox.php
+++ b/src/app/Handlers/Mailbox.php
@@ -36,4 +36,15 @@
*/
return true;
}
+
+ /**
+ * The priority that specifies the order of SKUs in UI.
+ * Higher number means higher on the list.
+ *
+ * @return int
+ */
+ public static function priority(): int
+ {
+ return 100;
+ }
}
diff --git a/src/app/Handlers/Storage.php b/src/app/Handlers/Storage.php
--- a/src/app/Handlers/Storage.php
+++ b/src/app/Handlers/Storage.php
@@ -14,8 +14,24 @@
public static function preReq($entitlement, $object)
{
+ if (!$entitlement->sku->active) {
+ \Log::error("Sku not active");
+ return false;
+ }
+
// TODO: The storage can not be modified to below what is already consumed.
return true;
}
+
+ /**
+ * The priority that specifies the order of SKUs in UI.
+ * Higher number means higher on the list.
+ *
+ * @return int
+ */
+ public static function priority(): int
+ {
+ return 90;
+ }
}
diff --git a/src/app/Http/Controllers/API/SkusController.php b/src/app/Http/Controllers/API/SkusController.php
--- a/src/app/Http/Controllers/API/SkusController.php
+++ b/src/app/Http/Controllers/API/SkusController.php
@@ -53,7 +53,7 @@
public function index()
{
$response = [];
- $skus = Sku::select()->orderBy('title')->get();
+ $skus = Sku::select()->get();
// Note: we do not limit the result to active SKUs only.
// It's because we might need users assigned to old SKUs,
@@ -65,6 +65,10 @@
}
}
+ usort($response, function ($a, $b) {
+ return ($b['prio'] <=> $a['prio']);
+ });
+
return response()->json($response);
}
@@ -137,6 +141,7 @@
$data['handler'] = $handler;
$data['readonly'] = false;
$data['enabled'] = false;
+ $data['prio'] = $sku->handler_class::priority();
// Use localized value, toArray() does not get them right
$data['name'] = $sku->name;
@@ -145,6 +150,14 @@
unset($data['handler_class']);
switch ($handler) {
+ case 'activesync':
+ $data['required'] = ['groupware'];
+ break;
+
+ case 'auth2f':
+ $data['forbidden'] = ['activesync'];
+ break;
+
case 'storage':
// Quota range input
$data['readonly'] = true; // only the checkbox will be disabled, not range
diff --git a/src/database/seeds/SkuSeeder.php b/src/database/seeds/SkuSeeder.php
--- a/src/database/seeds/SkuSeeder.php
+++ b/src/database/seeds/SkuSeeder.php
@@ -123,5 +123,31 @@
'active' => false,
]
);
+
+ Sku::create(
+ [
+ 'title' => '2fa',
+ 'name' => '2-Factor Authentication',
+ 'description' => 'Two factor authentication for webmail and administration panel',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Auth2F',
+ 'active' => true,
+ ]
+ );
+
+ Sku::create(
+ [
+ 'title' => 'activesync',
+ 'name' => 'Activesync',
+ 'description' => 'Mobile synchronization',
+ 'cost' => 100,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Activesync',
+ 'active' => true,
+ ]
+ );
}
}
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
@@ -97,7 +97,12 @@
<tbody>
<tr v-for="sku in skus" :id="'s' + sku.id" :key="sku.id">
<td class="selection">
- <input type="checkbox" :value="sku.id" :disabled="sku.readonly" :checked="sku.enabled">
+ <input type="checkbox" @input="onInputSku"
+ :value="sku.id"
+ :disabled="sku.readonly"
+ :checked="sku.enabled"
+ :dusk="'sku-input-' + sku.title"
+ >
</td>
<td class="name">
<span class="name">{{ sku.name }}</span>
@@ -235,6 +240,65 @@
}
})
},
+ onInputSku(e) {
+ let input = e.target
+ let sku = this.findSku(input.value)
+ let required = []
+
+ // We use 'readonly', not 'disabled', because we might want to handle
+ // input events. For example to display an error when someone clicks
+ // the locked input
+ if (input.readOnly) {
+ input.checked = !input.checked
+ // TODO: Display an alert explaining why it's locked
+ return
+ }
+
+ // TODO: Following code might not work if we change definition of forbidden/required
+ // or we just need more sophisticated SKU dependency rules
+
+ if (input.checked) {
+ // Check if a required SKU is selected, alert the user if not
+ (sku.required || []).forEach(title => {
+ this.skus.forEach(item => {
+ let checkbox
+ if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
+ if (!checkbox.checked) {
+ required.push(item.name)
+ }
+ }
+ })
+ })
+
+ if (required.length) {
+ input.checked = false
+ return alert(sku.name + ' requires ' + required.join(', ') + '.')
+ }
+ } else {
+ // Uncheck all dependent SKUs, e.g. when unchecking Groupware we also uncheck Activesync
+ // TODO: Should we display an alert instead?
+ this.skus.forEach(item => {
+ if (item.required && item.required.indexOf(sku.handler) > -1) {
+ $('#s' + item.id).find('input[type=checkbox]').prop('checked', false)
+ }
+ })
+ }
+
+ // Uncheck+lock/unlock conflicting SKUs
+ (sku.forbidden || []).forEach(title => {
+ this.skus.forEach(item => {
+ let checkbox
+ if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
+ if (input.checked) {
+ checkbox.checked = false
+ checkbox.readOnly = true
+ } else {
+ checkbox.readOnly = false
+ }
+ }
+ })
+ })
+ },
selectPackage(e) {
// Make sure there always is only one package selected
$('#user-packages input').prop('checked', false)
@@ -245,19 +309,20 @@
let value = input.val()
let record = input.parents('tr').first()
let sku_id = record.find('input[type=checkbox]').val()
- let sku, i
-
- for (i = 0; i < this.skus.length; i++) {
- if (this.skus[i].id == sku_id) {
- sku = this.skus[i];
- }
- }
+ let sku = this.findSku(sku_id)
// Update the label
input.prev().text(value + ' ' + sku.range.unit)
// Update the price
record.find('.price').text(this.$root.price(sku.cost * (value - sku.units_free)) + '/month')
+ },
+ findSku(id) {
+ for (let i = 0; i < this.skus.length; i++) {
+ if (this.skus[i].id == id) {
+ return this.skus[i];
+ }
+ }
}
}
}
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -70,6 +70,32 @@
}
/**
+ * Assert that the given element is readonly
+ */
+ public function assertReadonly($selector)
+ {
+ $element = $this->resolver->findOrFail($selector);
+ $value = $element->getAttribute('readonly');
+
+ Assert::assertTrue($value == 'true', "Element [$selector] is not readonly");
+
+ return $this;
+ }
+
+ /**
+ * Assert that the given element is not readonly
+ */
+ public function assertNotReadonly($selector)
+ {
+ $element = $this->resolver->findOrFail($selector);
+ $value = $element->getAttribute('readonly');
+
+ Assert::assertTrue($value != 'true', "Element [$selector] is not readonly");
+
+ return $this;
+ }
+
+ /**
* Assert that the given element contains specified text,
* no matter it's displayed or not.
*/
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -39,13 +39,7 @@
UserAlias::where('user_id', $john->id)
->where('alias', 'john.test@kolab.org')->delete();
- Sku::where('title', 'test')->delete();
- $storage = Sku::where('title', 'storage')->first();
- Entitlement::where([
- ['sku_id', $storage->id],
- ['entitleable_id', $john->id],
- ['cost', 25]
- ])->delete();
+ Entitlement::where('entitleable_id', $john->id)->whereIn('cost', [25, 100])->delete();
}
/**
@@ -60,13 +54,7 @@
UserAlias::where('user_id', $john->id)
->where('alias', 'john.test@kolab.org')->delete();
- Sku::where('title', 'test')->delete();
- $storage = Sku::where('title', 'storage')->first();
- Entitlement::where([
- ['sku_id', $storage->id],
- ['entitleable_id', $john->id],
- ['cost', 25]
- ])->delete();
+ Entitlement::where('entitleable_id', $john->id)->whereIn('cost', [25, 100])->delete();
parent::tearDown();
}
@@ -127,17 +115,6 @@
*/
public function testInfo(): void
{
- Sku::create([
- 'title' => 'test',
- 'name' => 'Test SKU',
- 'description' => 'The SKU for testing',
- 'cost' => 666,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Groupware',
- 'active' => true,
- ]);
-
$this->browse(function (Browser $browser) {
$browser->on(new UserList())
->click('@table tr:nth-child(2) a')
@@ -241,48 +218,57 @@
$browser->assertSeeIn('div.row:nth-child(8) label', 'Subscriptions')
->assertVisible('@skus.row:nth-child(8)')
->with('@skus', function ($browser) {
- $browser->assertElementsCount('tbody tr', 4)
- // groupware SKU
- ->assertSeeIn('tbody tr:nth-child(1) td.name', 'Groupware Features')
- ->assertSeeIn('tbody tr:nth-child(1) td.price', '5,55 CHF/month')
+ $browser->assertElementsCount('tbody tr', 5)
+ // Mailbox SKU
+ ->assertSeeIn('tbody tr:nth-child(1) td.name', 'User Mailbox')
+ ->assertSeeIn('tbody tr:nth-child(1) td.price', '4,44 CHF/month')
->assertChecked('tbody tr:nth-child(1) td.selection input')
- ->assertEnabled('tbody tr:nth-child(1) td.selection input')
+ ->assertDisabled('tbody tr:nth-child(1) td.selection input')
->assertTip(
'tbody tr:nth-child(1) td.buttons button',
- 'Groupware functions like Calendar, Tasks, Notes, etc.'
+ 'Just a mailbox'
)
- // Mailbox SKU
- ->assertSeeIn('tbody tr:nth-child(2) td.name', 'User Mailbox')
- ->assertSeeIn('tbody tr:nth-child(2) td.price', '4,44 CHF/month')
+ // Storage SKU
+ ->assertSeeIn('tbody tr:nth-child(2) td.name', 'Storage Quota')
+ ->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month')
->assertChecked('tbody tr:nth-child(2) td.selection input')
->assertDisabled('tbody tr:nth-child(2) td.selection input')
->assertTip(
'tbody tr:nth-child(2) td.buttons button',
- 'Just a mailbox'
+ 'Some wiggle room'
)
- // Storage SKU
- ->assertSeeIn('tbody tr:nth-child(3) td.name', 'Storage Quota')
- ->assertSeeIn('tr:nth-child(3) td.price', '0,00 CHF/month')
+ ->with(new QuotaInput('tbody tr:nth-child(2) .range-input'), function ($browser) {
+ $browser->assertQuotaValue(2)->setQuotaValue(3);
+ })
+ ->assertSeeIn('tr:nth-child(2) td.price', '0,25 CHF/month')
+ // groupware SKU
+ ->assertSeeIn('tbody tr:nth-child(3) td.name', 'Groupware Features')
+ ->assertSeeIn('tbody tr:nth-child(3) td.price', '5,55 CHF/month')
->assertChecked('tbody tr:nth-child(3) td.selection input')
- ->assertDisabled('tbody tr:nth-child(3) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(3) td.selection input')
->assertTip(
'tbody tr:nth-child(3) td.buttons button',
- 'Some wiggle room'
+ 'Groupware functions like Calendar, Tasks, Notes, etc.'
)
- ->with(new QuotaInput('tbody tr:nth-child(3) .range-input'), function ($browser) {
- $browser->assertQuotaValue(2)->setQuotaValue(3);
- })
- ->assertSeeIn('tr:nth-child(3) td.price', '0,25 CHF/month')
- // Test SKU
- ->assertSeeIn('tbody tr:nth-child(4) td.name', 'Test SKU')
- ->assertSeeIn('tbody tr:nth-child(4) td.price', '6,66 CHF/month')
+ // 2FA SKU
+ ->assertSeeIn('tbody tr:nth-child(4) td.name', '2-Factor Authentication')
+ ->assertSeeIn('tbody tr:nth-child(4) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(4) td.selection input')
->assertEnabled('tbody tr:nth-child(4) td.selection input')
->assertTip(
'tbody tr:nth-child(4) td.buttons button',
- 'The SKU for testing'
+ 'Two factor authentication for webmail and administration panel'
)
- ->click('tbody tr:nth-child(4) td.selection input');
+ // ActiveSync SKU
+ ->assertSeeIn('tbody tr:nth-child(5) td.name', 'Activesync')
+ ->assertSeeIn('tbody tr:nth-child(5) td.price', '1,00 CHF/month')
+ ->assertNotChecked('tbody tr:nth-child(5) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(5) td.selection input')
+ ->assertTip(
+ 'tbody tr:nth-child(5) td.buttons button',
+ 'Mobile synchronization'
+ )
+ ->click('tbody tr:nth-child(5) td.selection input');
})
->click('button[type=submit]');
})
@@ -292,7 +278,34 @@
->closeToast();
});
- $this->assertUserEntitlements($john, ['groupware', 'mailbox', 'storage', 'storage', 'storage', 'test']);
+ $expected = ['activesync', 'groupware', 'mailbox', 'storage', 'storage', 'storage'];
+ $this->assertUserEntitlements($john, $expected);
+
+ // Test subscriptions interaction
+ $browser->with('@form', function (Browser $browser) {
+ $browser->with('@skus', function ($browser) {
+ // Uncheck 'groupware', expect activesync unchecked
+ $browser->click('@sku-input-groupware')
+ ->assertNotChecked('@sku-input-groupware')
+ ->assertNotChecked('@sku-input-activesync')
+ ->assertEnabled('@sku-input-activesync')
+ ->assertNotReadonly('@sku-input-activesync')
+ // Check 'activesync', expect an alert
+ ->click('@sku-input-activesync')
+ ->assertDialogOpened('Activesync requires Groupware Features.')
+ ->acceptDialog()
+ ->assertNotChecked('@sku-input-activesync')
+ // Check '2FA', expect 'activesync' unchecked and readonly
+ ->click('@sku-input-2fa')
+ ->assertChecked('@sku-input-2fa')
+ ->assertNotChecked('@sku-input-activesync')
+ ->assertReadonly('@sku-input-activesync')
+ // Uncheck '2FA'
+ ->click('@sku-input-2fa')
+ ->assertNotChecked('@sku-input-2fa')
+ ->assertNotReadonly('@sku-input-activesync');
+ });
+ });
});
}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -18,25 +18,26 @@
$response->assertStatus(401);
$user = $this->getTestUser('john@kolab.org');
- $domain_sku = Sku::where('title', 'domain')->first();
+ $sku = Sku::where('title', 'mailbox')->first();
$response = $this->actingAs($user)->get("api/v4/skus");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(7, $json);
+ $this->assertCount(9, $json);
- $this->assertSame($domain_sku->id, $json[0]['id']);
- $this->assertSame($domain_sku->title, $json[0]['title']);
- $this->assertSame($domain_sku->name, $json[0]['name']);
- $this->assertSame($domain_sku->description, $json[0]['description']);
- $this->assertSame($domain_sku->cost, $json[0]['cost']);
- $this->assertSame($domain_sku->units_free, $json[0]['units_free']);
- $this->assertSame($domain_sku->period, $json[0]['period']);
- $this->assertSame($domain_sku->active, $json[0]['active']);
- $this->assertSame('domain', $json[0]['type']);
- $this->assertSame('domain', $json[0]['handler']);
+ $this->assertSame(100, $json[0]['prio']);
+ $this->assertSame($sku->id, $json[0]['id']);
+ $this->assertSame($sku->title, $json[0]['title']);
+ $this->assertSame($sku->name, $json[0]['name']);
+ $this->assertSame($sku->description, $json[0]['description']);
+ $this->assertSame($sku->cost, $json[0]['cost']);
+ $this->assertSame($sku->units_free, $json[0]['units_free']);
+ $this->assertSame($sku->period, $json[0]['period']);
+ $this->assertSame($sku->active, $json[0]['active']);
+ $this->assertSame('user', $json[0]['type']);
+ $this->assertSame('mailbox', $json[0]['handler']);
}
/**
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 2:01 PM (6 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18824042
Default Alt Text
D1057.1775311307.diff (23 KB)
Attached To
Mode
D1057: Add 2FA and Activesync SKUs
Attached
Detach File
Event Timeline