Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117819980
D1714.1775293955.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
46 KB
Referenced Files
None
Subscribers
None
D1714.1775293955.diff
View Options
diff --git a/src/app/Console/Commands/Sku/ListUsers.php b/src/app/Console/Commands/Sku/ListUsers.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Sku/ListUsers.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Console\Commands\Sku;
+
+use Illuminate\Console\Command;
+
+class ListUsers extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'sku:list-users {sku}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'List users with the SKU entitlement.';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $sku = \App\Sku::find($this->argument('sku'));
+
+ if (!$sku) {
+ $sku = \App\Sku::where('title', $this->argument('sku'))->first();
+ }
+
+ if (!$sku) {
+ $this->error("Unable to find the SKU.");
+ return 1;
+ }
+
+ $fn = function ($entitlement) {
+ $user_id = $entitlement->user_id;
+ if ($entitlement->entitleable_type == \App\User::class) {
+ $user_id = $entitlement->entitleable_id;
+ }
+
+ return $user_id;
+ };
+
+ $users = \App\Entitlement::select('user_id', 'entitleable_id', 'entitleable_type')
+ ->join('wallets', 'wallets.id', '=', 'wallet_id')
+ ->where('sku_id', $sku->id)
+ ->get()
+ ->map($fn)
+ ->unique();
+
+ // TODO: This wereIn() might not scale
+ \App\User::whereIn('id', $users)->orderBy('email')->get()
+ ->pluck('email')
+ ->each(function ($email, $key) {
+ $this->info($email);
+ });
+ }
+}
diff --git a/src/app/Console/Commands/UserAssignSku.php b/src/app/Console/Commands/UserAssignSku.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/UserAssignSku.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class UserAssignSku extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:assign-sku {user} {sku} {--qty=}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Assign a SKU to the user';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = \App\User::where('email', $this->argument('user'))->first();
+
+ if (!$user) {
+ $this->error("Unable to find the user {$this->argument('user')}.");
+ return 1;
+ }
+
+ $sku = \App\Sku::find($this->argument('sku'));
+
+ if (!$sku) {
+ $sku = \App\Sku::where('title', $this->argument('sku'))->first();
+ }
+
+ if (!$sku) {
+ $this->error("Unable to find the SKU {$this->argument('sku')}.");
+ return 1;
+ }
+
+ $quantity = (int) $this->option('qty');
+
+ // Check if the entitlement already exists
+ if (empty($quantity)) {
+ if ($user->entitlements()->where('sku_id', $sku->id)->first()) {
+ $this->error("The entitlement already exists. Maybe try with --qty=X?");
+ return 1;
+ }
+ }
+
+ $user->assignSku($sku, $quantity ?: 1);
+ }
+}
diff --git a/src/app/Handlers/Activesync.php b/src/app/Handlers/Activesync.php
--- a/src/app/Handlers/Activesync.php
+++ b/src/app/Handlers/Activesync.php
@@ -4,21 +4,38 @@
class Activesync extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $object): bool
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
{
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
+ $data = parent::metadata($sku);
- return true;
+ $data['required'] = ['groupware'];
+
+ return $data;
}
+ /**
+ * 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 70;
diff --git a/src/app/Handlers/Auth2F.php b/src/app/Handlers/Auth2F.php
--- a/src/app/Handlers/Auth2F.php
+++ b/src/app/Handlers/Auth2F.php
@@ -4,21 +4,38 @@
class Auth2F extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $object): bool
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
{
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
+ $data = parent::metadata($sku);
- return true;
+ $data['forbidden'] = ['activesync'];
+
+ return $data;
}
+ /**
+ * 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 60;
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
@@ -15,6 +15,56 @@
}
/**
+ * Check if the SKU is available to the user. An SKU is available
+ * to the user when either it is active or there's already an
+ * active entitlement.
+ *
+ * @param \App\Sku $sku The SKU object
+ * @param \App\User $user The user object
+ *
+ * @return bool
+ */
+ public static function isAvailable(\App\Sku $sku, \App\User $user): bool
+ {
+ if (!$sku->active) {
+ if (!$user->entitlements()->where('sku_id', $sku->id)->first()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Metadata of this SKU handler.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
+ {
+ $handler = explode('\\', static::class);
+ $handler = strtolower(end($handler));
+
+ $type = explode('\\', static::entitleableClass());
+ $type = strtolower(end($type));
+
+ return [
+ // entitleable type
+ 'type' => $type,
+ // handler (as a keyword)
+ 'handler' => $handler,
+ // readonly entitlement state cannot be changed
+ 'readonly' => false,
+ // is entitlement enabled by default?
+ 'enabled' => false,
+ // priority on the entitlements list
+ 'prio' => static::priority(),
+ ];
+ }
+
+ /**
* Prerequisites for the Entitlement to be applied to the object.
*
* @param \App\Entitlement $entitlement
@@ -24,6 +74,23 @@
*/
public static function preReq($entitlement, $object): bool
{
+ $type = static::entitleableClass();
+
+ if (empty($type) || empty($entitlement->entitleable_type)) {
+ \Log::error("Entitleable class/type not specified");
+ return false;
+ }
+
+ if ($type !== $entitlement->entitleable_type) {
+ \Log::error("Entitleable class mismatch");
+ return false;
+ }
+
+ if (!$entitlement->sku->active) {
+ \Log::error("Sku not active");
+ return false;
+ }
+
return true;
}
diff --git a/src/app/Handlers/Base.php b/src/app/Handlers/Beta.php
copy from src/app/Handlers/Base.php
copy to src/app/Handlers/Beta.php
--- a/src/app/Handlers/Base.php
+++ b/src/app/Handlers/Beta.php
@@ -2,7 +2,7 @@
namespace App\Handlers;
-abstract class Base
+class Beta extends \App\Handlers\Base
{
/**
* The entitleable class for this handler.
@@ -11,7 +11,7 @@
*/
public static function entitleableClass(): string
{
- return '';
+ return \App\User::class;
}
/**
@@ -24,6 +24,13 @@
*/
public static function preReq($entitlement, $object): bool
{
+ // We allow inactive "beta" Sku to be assigned
+
+ if (self::entitleableClass() !== $entitlement->entitleable_type) {
+ \Log::error("Entitleable class mismatch");
+ return false;
+ }
+
return true;
}
@@ -35,6 +42,7 @@
*/
public static function priority(): int
{
- return 0;
+ // Just above all other beta SKUs, please
+ return 10;
}
}
diff --git a/src/app/Handlers/Beta/Base.php b/src/app/Handlers/Beta/Base.php
new file mode 100644
--- /dev/null
+++ b/src/app/Handlers/Beta/Base.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Handlers\Beta;
+
+class Base extends \App\Handlers\Base
+{
+ /**
+ * Check if the SKU is available to the user.
+ *
+ * @param \App\Sku $sku The SKU object
+ * @param \App\User $user The user object
+ *
+ * @return bool
+ */
+ public static function isAvailable(\App\Sku $sku, \App\User $user): bool
+ {
+ // These SKUs must be:
+ // 1) already assigned or
+ // 2) active and a 'beta' entitlement must exist.
+
+ if ($sku->active) {
+ $beta = \App\Sku::where('title', 'beta')->first();
+ if (!$beta) {
+ return false;
+ }
+
+ if ($user->entitlements()->where('sku_id', $beta->id)->first()) {
+ return true;
+ }
+ } else {
+ if ($user->entitlements()->where('sku_id', $sku->id)->first()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
+ {
+ $data = parent::metadata($sku);
+
+ $data['required'] = ['beta'];
+
+ return $data;
+ }
+
+ /**
+ * Prerequisites for the Entitlement to be applied to the object.
+ *
+ * @param \App\Entitlement $entitlement
+ * @param mixed $object
+ *
+ * @return bool
+ */
+ public static function preReq($entitlement, $object): bool
+ {
+ if (!parent::preReq($entitlement, $object)) {
+ return false;
+ }
+
+ // TODO: User has to have the "beta" entitlement
+
+ return true;
+ }
+}
diff --git a/src/app/Handlers/Beta/Meet.php b/src/app/Handlers/Beta/Meet.php
new file mode 100644
--- /dev/null
+++ b/src/app/Handlers/Beta/Meet.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace App\Handlers\Beta;
+
+class Meet extends Base
+{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
+ public static function entitleableClass(): string
+ {
+ // Note: We connot just inherit from the parent because
+ // we use static:: there.
+ return \App\User::class;
+ }
+}
diff --git a/src/app/Handlers/Domain.php b/src/app/Handlers/Domain.php
--- a/src/app/Handlers/Domain.php
+++ b/src/app/Handlers/Domain.php
@@ -4,18 +4,13 @@
class Domain extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\Domain::class;
}
-
- public static function preReq($entitlement, $domain): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/DomainHosting.php b/src/app/Handlers/DomainHosting.php
--- a/src/app/Handlers/DomainHosting.php
+++ b/src/app/Handlers/DomainHosting.php
@@ -4,18 +4,13 @@
class DomainHosting extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\Domain::class;
}
-
- public static function preReq($entitlement, $domain): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/DomainRegistration.php b/src/app/Handlers/DomainRegistration.php
--- a/src/app/Handlers/DomainRegistration.php
+++ b/src/app/Handlers/DomainRegistration.php
@@ -4,18 +4,13 @@
class DomainRegistration extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\Domain::class;
}
-
- public static function preReq($entitlement, $domain): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/DomainRelay.php b/src/app/Handlers/DomainRelay.php
--- a/src/app/Handlers/DomainRelay.php
+++ b/src/app/Handlers/DomainRelay.php
@@ -4,18 +4,13 @@
class DomainRelay extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\Domain::class;
}
-
- public static function preReq($entitlement, $domain): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
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
@@ -4,21 +4,16 @@
class Groupware extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $user): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
-
/**
* The priority that specifies the order of SKUs in UI.
* Higher number means higher on the list.
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
@@ -4,37 +4,32 @@
class Mailbox extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $user): bool
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
{
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-/*
- FIXME: This code prevents from creating initial mailbox SKU
- on signup of group account, because User::domains()
- does not return the new domain.
- Either we make sure to create domain entitlement before mailbox
- entitlement or make the method here aware of that case or?
-
- list($local, $domain) = explode('@', $user->email);
-
- $domains = $user->domains();
+ $data = parent::metadata($sku);
- foreach ($domains as $_domain) {
- if ($domain == $_domain->namespace) {
- return true;
- }
- }
+ // Mailbox is always enabled and cannot be unset
+ $data['readonly'] = true;
+ $data['enabled'] = true;
- \Log::info("Domain not for user");
-*/
- return true;
+ return $data;
}
/**
diff --git a/src/app/Handlers/Resource.php b/src/app/Handlers/Resource.php
--- a/src/app/Handlers/Resource.php
+++ b/src/app/Handlers/Resource.php
@@ -4,19 +4,14 @@
class Resource extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
// TODO
return '';
}
-
- public static function preReq($entitlement, $owner): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
diff --git a/src/app/Handlers/SharedFolder.php b/src/app/Handlers/SharedFolder.php
--- a/src/app/Handlers/SharedFolder.php
+++ b/src/app/Handlers/SharedFolder.php
@@ -4,19 +4,14 @@
class SharedFolder extends \App\Handlers\Base
{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
// TODO
return '';
}
-
- public static function preReq($entitlement, $owner): bool
- {
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
-
- return true;
- }
}
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
@@ -7,21 +7,36 @@
public const MAX_ITEMS = 100;
public const ITEM_UNIT = 'GB';
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
public static function entitleableClass(): string
{
return \App\User::class;
}
- public static function preReq($entitlement, $object): bool
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
{
- if (!$entitlement->sku->active) {
- \Log::error("Sku not active");
- return false;
- }
+ $data = parent::metadata($sku);
- // TODO: The storage can not be modified to below what is already consumed.
+ $data['readonly'] = true; // only the checkbox will be disabled, not range
+ $data['enabled'] = true;
+ $data['range'] = [
+ 'min' => $sku->units_free,
+ 'max' => self::MAX_ITEMS,
+ 'unit' => self::ITEM_UNIT,
+ ];
- return true;
+ return $data;
}
/**
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -5,6 +5,7 @@
use App\Http\Controllers\Controller;
use App\Sku;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
class SkusController extends Controller
{
@@ -46,18 +47,14 @@
}
/**
- * Display a listing of the sku.
+ * Get a list of active SKUs.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
// Note: Order by title for consistent ordering in tests
- $skus = Sku::select()->orderBy('title')->get();
-
- // Note: we do not limit the result to active SKUs only.
- // It's because we might need users assigned to old SKUs,
- // we need to display these old SKUs on the entitlements list
+ $skus = Sku::where('active', true)->orderBy('title')->get();
$response = [];
@@ -115,6 +112,52 @@
}
/**
+ * Get a list of SKUs available to the user.
+ *
+ * @param int $id User identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function userSkus($id)
+ {
+ $user = \App\User::find($id);
+
+ if (empty($user)) {
+ return $this->errorResponse(404);
+ }
+
+ if (!Auth::guard()->user()->canRead($user)) {
+ return $this->errorResponse(403);
+ }
+
+ $type = request()->input('type');
+ $response = [];
+
+ // Note: Order by title for consistent ordering in tests
+ $skus = Sku::orderBy('title')->get();
+
+ foreach ($skus as $sku) {
+ if (!$sku->handler_class::isAvailable($sku, $user)) {
+ continue;
+ }
+
+ if ($data = $this->skuElement($sku)) {
+ if ($type && $type != $data['type']) {
+ continue;
+ }
+
+ $response[] = $data;
+ }
+ }
+
+ usort($response, function ($a, $b) {
+ return ($b['prio'] <=> $a['prio']);
+ });
+
+ return response()->json($response);
+ }
+
+ /**
* Convert SKU information to metadata used by UI to
* display the form control
*
@@ -124,59 +167,18 @@
*/
protected function skuElement($sku): ?array
{
- $type = $sku->handler_class::entitleableClass();
+ $data = array_merge($sku->toArray(), $sku->handler_class::metadata($sku));
// ignore incomplete handlers
- if (!$type) {
+ if (empty($data['type'])) {
return null;
}
- $type = explode('\\', $type);
- $type = strtolower(end($type));
-
- $handler = explode('\\', $sku->handler_class);
- $handler = strtolower(end($handler));
-
- $data = $sku->toArray();
-
- $data['type'] = $type;
- $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;
$data['description'] = $sku->description;
- 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
- $data['enabled'] = true;
- $data['range'] = [
- 'min' => $data['units_free'],
- 'max' => $sku->handler_class::MAX_ITEMS,
- 'unit' => $sku->handler_class::ITEM_UNIT,
- ];
- break;
-
- case 'mailbox':
- // Mailbox is always enabled and cannot be unset
- $data['readonly'] = true;
- $data['enabled'] = true;
- break;
- }
+ unset($data['handler_class'], $data['created_at'], $data['updated_at']);
return $data;
}
diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php
--- a/src/app/Observers/EntitlementObserver.php
+++ b/src/app/Observers/EntitlementObserver.php
@@ -24,14 +24,6 @@
*/
public function creating(Entitlement $entitlement): bool
{
- while (true) {
- $allegedly_unique = \App\Utils::uuidStr();
- if (!Entitlement::find($allegedly_unique)) {
- $entitlement->{$entitlement->getKeyName()} = $allegedly_unique;
- break;
- }
- }
-
// can't dispatch job here because it'll fail serialization
// Make sure the owner is at least a controller on the wallet
@@ -53,6 +45,14 @@
return false;
}
+ while (true) {
+ $allegedly_unique = \App\Utils::uuidStr();
+ if (!Entitlement::find($allegedly_unique)) {
+ $entitlement->{$entitlement->getKeyName()} = $allegedly_unique;
+ break;
+ }
+ }
+
return true;
}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -175,12 +175,6 @@
$wallet = $this->wallet();
$exists = $this->entitlements()->where('sku_id', $sku->id)->count();
- // TODO: Sanity check, this probably should be in preReq() on handlers
- // or in EntitlementObserver
- if ($sku->handler_class::entitleableClass() != User::class) {
- throw new \Exception("Cannot assign non-user SKU ({$sku->title}) to a user");
- }
-
while ($count > 0) {
\App\Entitlement::create([
'wallet_id' => $wallet->id,
diff --git a/src/database/seeds/local/SkuSeeder.php b/src/database/seeds/local/SkuSeeder.php
--- a/src/database/seeds/local/SkuSeeder.php
+++ b/src/database/seeds/local/SkuSeeder.php
@@ -151,5 +151,31 @@
'active' => true,
]
);
+
+ Sku::create(
+ [
+ 'title' => 'beta',
+ 'name' => 'Beta program',
+ 'description' => 'Access to beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ]
+ );
+
+ Sku::create(
+ [
+ 'title' => 'meet',
+ 'name' => 'Video chat',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta\Meet',
+ 'active' => true,
+ ]
+ );
}
}
diff --git a/src/database/seeds/production/SkuSeeder.php b/src/database/seeds/production/SkuSeeder.php
--- a/src/database/seeds/production/SkuSeeder.php
+++ b/src/database/seeds/production/SkuSeeder.php
@@ -151,5 +151,31 @@
'active' => true,
]
);
+
+ Sku::create(
+ [
+ 'title' => 'beta',
+ 'name' => 'Beta program',
+ 'description' => 'Access to beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ]
+ );
+
+ Sku::create(
+ [
+ 'title' => 'meet',
+ 'name' => 'Video chat',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta\Meet',
+ 'active' => true,
+ ]
+ );
}
}
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
@@ -450,7 +450,7 @@
})
// Create subscriptions list
- axios.get('/api/v4/skus')
+ axios.get('/api/v4/users/' + user_id + '/skus')
.then(response => {
// "merge" SKUs with user entitlement-SKUs
response.data.forEach(sku => {
@@ -470,7 +470,7 @@
this.skus.push(item)
- if (sku.title == '2fa') {
+ if (sku.handler == 'auth2f') {
this.has2FA = true
this.sku2FA = sku.id
}
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
@@ -219,11 +219,10 @@
this.discount_description = this.user.wallet.discount_description
this.status = response.data.statusInfo
- axios.get('/api/v4/skus')
+ axios.get('/api/v4/users/' + this.user_id + '/skus?type=user')
.then(response => {
// "merge" SKUs with user entitlement-SKUs
this.skus = response.data
- .filter(sku => sku.type == 'user')
.map(sku => {
if (sku.id in this.user.skus) {
sku.enabled = true
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -67,6 +67,7 @@
Route::apiResource('packages', API\V4\PackagesController::class);
Route::apiResource('skus', API\V4\SkusController::class);
Route::apiResource('users', API\V4\UsersController::class);
+ Route::get('users/{id}/skus', 'API\V4\SkusController@userSkus');
Route::get('users/{id}/status', 'API\V4\UsersController@status');
Route::apiResource('wallets', API\V4\WalletsController::class);
@@ -120,6 +121,7 @@
Route::apiResource('skus', API\V4\Admin\SkusController::class);
Route::apiResource('users', API\V4\Admin\UsersController::class);
Route::post('users/{id}/reset2FA', 'API\V4\Admin\UsersController@reset2FA');
+ Route::get('users/{id}/skus', 'API\V4\Admin\SkusController@userSkus');
Route::post('users/{id}/suspend', 'API\V4\Admin\UsersController@suspend');
Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend');
Route::apiResource('wallets', API\V4\Admin\WalletsController::class);
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
@@ -46,6 +46,9 @@
$wallet = $john->wallets()->first();
$wallet->discount()->dissociate();
$wallet->save();
+
+ $betas = Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ Entitlement::whereIn('sku_id', $betas)->delete();
}
/**
@@ -66,6 +69,9 @@
$wallet->discount()->dissociate();
$wallet->save();
+ $betas = Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ Entitlement::whereIn('sku_id', $betas)->delete();
+
parent::tearDown();
}
@@ -583,4 +589,68 @@
});
});
}
+
+ /**
+ * Test beta entitlements
+ *
+ * @depends testList
+ */
+ public function testBetaEntitlements(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $john = User::where('email', 'john@kolab.org')->first();
+ $sku = Sku::where('title', 'beta')->first();
+ $john->assignSku($sku);
+
+ $browser->visit('/user/' . $john->id)
+ ->on(new UserInfo())
+ ->with('@skus', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 7)
+ // Beta SKU
+ ->assertSeeIn('tbody tr:nth-child(6) td.name', 'Beta program')
+ ->assertSeeIn('tbody tr:nth-child(6) td.price', '0,00 CHF/month')
+ ->assertChecked('tbody tr:nth-child(6) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(6) td.selection input')
+ ->assertTip(
+ 'tbody tr:nth-child(6) td.buttons button',
+ 'Access to beta program subscriptions'
+ )
+ // Beta/Meet SKU
+ ->assertSeeIn('tbody tr:nth-child(7) td.name', 'Video chat')
+ ->assertSeeIn('tr:nth-child(7) td.price', '0,00 CHF/month')
+ ->assertNotChecked('tbody tr:nth-child(7) td.selection input')
+ ->assertEnabled('tbody tr:nth-child(7) td.selection input')
+ ->assertTip(
+ 'tbody tr:nth-child(7) td.buttons button',
+ 'Video conferencing tool'
+ )
+ // Check Meet, Uncheck Beta, expect Meet unchecked
+ ->click('#sku-input-meet')
+ ->click('#sku-input-beta')
+ ->assertNotChecked('#sku-input-beta')
+ ->assertNotChecked('#sku-input-meet')
+ // Click Meet expect an alert
+ ->click('#sku-input-meet')
+ ->assertDialogOpened('Video chat requires Beta program.')
+ ->acceptDialog()
+ // Enable Meet and Beta and submit
+ ->click('#sku-input-beta')
+ ->click('#sku-input-meet');
+ })
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
+
+ $expected = ['beta', 'groupware', 'mailbox', 'meet', 'storage', 'storage'];
+ $this->assertUserEntitlements($john, $expected);
+
+ $browser->visit('/user/' . $john->id)
+ ->on(new UserInfo())
+ ->click('#sku-input-beta')
+ ->click('button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
+
+ $expected = ['groupware', 'mailbox', 'storage', 'storage'];
+ $this->assertUserEntitlements($john, $expected);
+ });
+ }
}
diff --git a/src/tests/Feature/Console/Sku/ListUsersTest.php b/src/tests/Feature/Console/Sku/ListUsersTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Sku/ListUsersTest.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Tests\Feature\Console\Sku;
+
+use Illuminate\Contracts\Console\Kernel;
+use Tests\TestCase;
+
+class ListUsersTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('sku-list-users@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('sku-list-users@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+ $code = \Artisan::call('sku:list-users meet');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+
+ $code = \Artisan::call('sku:list-users unknown');
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("Unable to find the SKU.", $output);
+
+ $code = \Artisan::call('sku:list-users 2fa');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame("ned@kolab.org", $output);
+
+ $code = \Artisan::call('sku:list-users mailbox');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame("jack@kolab.org\njoe@kolab.org\njohn@kolab.org\nned@kolab.org", $output);
+
+ $code = \Artisan::call('sku:list-users domain-hosting');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame("john@kolab.org", $output);
+
+ $sku = \App\Sku::where('title', 'meet')->first();
+ $user = $this->getTestUser('sku-list-users@kolabnow.com');
+ $user->assignSku($sku);
+
+ $code = \Artisan::call('sku:list-users meet');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame($user->email, $output);
+
+ $user->assignSku($sku);
+
+ $code = \Artisan::call('sku:list-users meet');
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame($user->email, $output);
+ }
+}
diff --git a/src/tests/Feature/Console/UserAssignSkuTest.php b/src/tests/Feature/Console/UserAssignSkuTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/UserAssignSkuTest.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Tests\Feature\Console;
+
+use Tests\TestCase;
+
+class UserAssignSkuTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('add-entitlement@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('add-entitlement@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ $sku = \App\Sku::where('title', 'meet')->first();
+ $user = $this->getTestUser('add-entitlement@kolabnow.com');
+
+ $this->artisan('user:assign-sku unknown@unknown.org ' . $sku->id)
+ ->assertExitCode(1)
+ ->expectsOutput("Unable to find the user unknown@unknown.org.");
+
+ $this->artisan('user:assign-sku ' . $user->email . ' unknownsku')
+ ->assertExitCode(1)
+ ->expectsOutput("Unable to find the SKU unknownsku.");
+
+ $this->artisan('user:assign-sku ' . $user->email . ' ' . $sku->id)
+ ->assertExitCode(0);
+
+ $this->assertCount(1, $user->entitlements()->where('sku_id', $sku->id)->get());
+
+ // Try again (also test sku by title)
+ $this->artisan('user:assign-sku ' . $user->email . ' ' . $sku->title)
+ ->assertExitCode(1)
+ ->expectsOutput("The entitlement already exists. Maybe try with --qty=X?");
+
+ $this->assertCount(1, $user->entitlements()->where('sku_id', $sku->id)->get());
+
+ // Try again with --qty option, to force the assignment
+ $this->artisan('user:assign-sku ' . $user->email . ' ' . $sku->title . ' --qty=1')
+ ->assertExitCode(0);
+
+ $this->assertCount(2, $user->entitlements()->where('sku_id', $sku->id)->get());
+ }
+}
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
@@ -2,6 +2,7 @@
namespace Tests\Feature\Controller;
+use App\Entitlement;
use App\Http\Controllers\API\V4\SkusController;
use App\Sku;
use Tests\TestCase;
@@ -9,6 +10,29 @@
class SkusTest extends TestCase
{
/**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $betas = Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ Entitlement::whereIn('sku_id', $betas)->delete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $betas = Sku::where('handler_class', 'like', '%\\Beta%')->pluck('id')->all();
+ Entitlement::whereIn('sku_id', $betas)->delete();
+
+ parent::tearDown();
+ }
+
+
+ /**
* Test fetching SKUs list
*/
public function testIndex(): void
@@ -25,7 +49,7 @@
$json = $response->json();
- $this->assertCount(9, $json);
+ $this->assertCount(7, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
@@ -41,12 +65,125 @@
}
/**
- * Test for SkusController::skuElement()
+ * Test fetching SKUs list for a user (GET /users/<id>/skus)
*/
- public function testSkuElement(): void
+ public function testUserSkus(): void
{
- $sku = Sku::where('title', 'storage')->first();
- $result = $this->invokeMethod(new SkusController(), 'skuElement', [$sku]);
+ $user = $this->getTestUser('john@kolab.org');
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/users/{$user->id}/skus");
+ $response->assertStatus(401);
+
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(6, $json);
+
+ $this->assertSkuElement('mailbox', $json[0], [
+ 'prio' => 100,
+ 'type' => 'user',
+ 'handler' => 'mailbox',
+ 'enabled' => true,
+ 'readonly' => true,
+ ]);
+
+ $this->assertSkuElement('storage', $json[1], [
+ 'prio' => 90,
+ 'type' => 'user',
+ 'handler' => 'storage',
+ 'enabled' => true,
+ 'readonly' => true,
+ 'range' => [
+ 'min' => 2,
+ 'max' => 100,
+ 'unit' => 'GB',
+ ]
+ ]);
+
+ $this->assertSkuElement('groupware', $json[2], [
+ 'prio' => 80,
+ 'type' => 'user',
+ 'handler' => 'groupware',
+ 'enabled' => false,
+ 'readonly' => false,
+ ]);
+
+ $this->assertSkuElement('activesync', $json[3], [
+ 'prio' => 70,
+ 'type' => 'user',
+ 'handler' => 'activesync',
+ 'enabled' => false,
+ 'readonly' => false,
+ 'required' => ['groupware'],
+ ]);
+
+ $this->assertSkuElement('2fa', $json[4], [
+ 'prio' => 60,
+ 'type' => 'user',
+ 'handler' => 'auth2f',
+ 'enabled' => false,
+ 'readonly' => false,
+ 'forbidden' => ['activesync'],
+ ]);
+
+ $this->assertSkuElement('domain-hosting', $json[5], [
+ 'prio' => 0,
+ 'type' => 'domain',
+ 'handler' => 'domainhosting',
+ 'enabled' => false,
+ 'readonly' => false,
+ ]);
+
+ // Test filter by type
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus?type=domain");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ $this->assertSame('domain', $json[0]['type']);
+
+ // Test inclusion of beta SKUs
+ $sku = Sku::where('title', 'beta')->first();
+ $user->assignSku($sku);
+ $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus?type=user");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(7, $json);
+
+ $this->assertSkuElement('beta', $json[5], [
+ 'prio' => 10,
+ 'type' => 'user',
+ 'handler' => 'beta',
+ 'enabled' => false,
+ 'readonly' => false,
+ ]);
+
+ $this->assertSkuElement('meet', $json[6], [
+ 'prio' => 0,
+ 'type' => 'user',
+ 'handler' => 'meet',
+ 'enabled' => false,
+ 'readonly' => false,
+ 'required' => ['beta'],
+ ]);
+ }
+
+ /**
+ * Assert content of the SKU element in an API response
+ *
+ * @param string $sku_title The SKU title
+ * @param array $result The result to assert
+ * @param array $other Other items the SKU itself does not include
+ */
+ protected function assertSkuElement($sku_title, $result, $other = []): void
+ {
+ $sku = Sku::where('title', $sku_title)->first();
$this->assertSame($sku->id, $result['id']);
$this->assertSame($sku->title, $result['title']);
@@ -56,15 +193,11 @@
$this->assertSame($sku->units_free, $result['units_free']);
$this->assertSame($sku->period, $result['period']);
$this->assertSame($sku->active, $result['active']);
- $this->assertSame('user', $result['type']);
- $this->assertSame('storage', $result['handler']);
- $this->assertSame($sku->units_free, $result['range']['min']);
- $this->assertSame($sku->handler_class::MAX_ITEMS, $result['range']['max']);
- $this->assertSame($sku->handler_class::ITEM_UNIT, $result['range']['unit']);
- $this->assertTrue($result['readonly']);
- $this->assertTrue($result['enabled']);
-
- // Test all SKU types
- $this->markTestIncomplete();
+
+ foreach ($other as $key => $value) {
+ $this->assertSame($value, $result[$key]);
+ }
+
+ $this->assertCount(8 + count($other), $result);
}
}
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -6,8 +6,6 @@
use App\Entitlement;
use App\Sku;
use App\User;
-use Illuminate\Foundation\Testing\WithFaker;
-use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
diff --git a/src/tests/Feature/SignupCodeTest.php b/src/tests/Feature/SignupCodeTest.php
--- a/src/tests/Feature/SignupCodeTest.php
+++ b/src/tests/Feature/SignupCodeTest.php
@@ -5,8 +5,6 @@
use App\SignupCode;
use Carbon\Carbon;
use Tests\TestCase;
-use Illuminate\Foundation\Testing\WithFaker;
-use Illuminate\Foundation\Testing\RefreshDatabase;
class SignupCodeTest extends TestCase
{
diff --git a/src/tests/Feature/VerificationCodeTest.php b/src/tests/Feature/VerificationCodeTest.php
--- a/src/tests/Feature/VerificationCodeTest.php
+++ b/src/tests/Feature/VerificationCodeTest.php
@@ -6,8 +6,6 @@
use App\VerificationCode;
use Carbon\Carbon;
use Tests\TestCase;
-use Illuminate\Foundation\Testing\WithFaker;
-use Illuminate\Foundation\Testing\RefreshDatabase;
class VerificationCodeTest extends TestCase
{
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -8,8 +8,6 @@
use App\Wallet;
use Carbon\Carbon;
use Tests\TestCase;
-use Illuminate\Foundation\Testing\WithFaker;
-use Illuminate\Foundation\Testing\RefreshDatabase;
class WalletTest extends TestCase
{
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 9:12 AM (18 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828733
Default Alt Text
D1714.1775293955.diff (46 KB)
Attached To
Mode
D1714: Beta SKUs
Attached
Detach File
Event Timeline