Page MenuHomePhorge

No OneTemporary

Size
205 KB
Referenced Files
None
Subscribers
None
diff --git a/src/app/Handlers/Activesync.php b/src/app/Handlers/Activesync.php
index 98e3fc0a..9864c934 100644
--- a/src/app/Handlers/Activesync.php
+++ b/src/app/Handlers/Activesync.php
@@ -1,43 +1,43 @@
<?php
namespace App\Handlers;
class Activesync extends \App\Handlers\Base
{
/**
* The entitleable class for this handler.
*
* @return string
*/
public static function entitleableClass(): string
{
return \App\User::class;
}
/**
* 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'] = ['groupware'];
+ $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
index a7651e83..d17a40d1 100644
--- a/src/app/Handlers/Auth2F.php
+++ b/src/app/Handlers/Auth2F.php
@@ -1,43 +1,43 @@
<?php
namespace App\Handlers;
class Auth2F extends \App\Handlers\Base
{
/**
* The entitleable class for this handler.
*
* @return string
*/
public static function entitleableClass(): string
{
return \App\User::class;
}
/**
* 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['forbidden'] = ['activesync'];
+ $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
index f01487ba..87041e50 100644
--- a/src/app/Handlers/Base.php
+++ b/src/app/Handlers/Base.php
@@ -1,107 +1,101 @@
<?php
namespace App\Handlers;
abstract class Base
{
/**
* The entitleable class for this handler.
*
* @return string
*/
public static function entitleableClass(): string
{
return '';
}
/**
* Check if the SKU is available to the user. An SKU is available
* to the user/domain when either it is active or there's already an
* active entitlement.
*
* @param \App\Sku $sku The SKU object
* @param \App\User|\App\Domain $object The user or domain object
*
* @return bool
*/
public static function isAvailable(\App\Sku $sku, $object): bool
{
if (!$sku->active) {
if (!$object->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,
+ 'type' => \lcfirst(\class_basename(static::entitleableClass())),
+ // handler
+ 'handler' => str_replace("App\\Handlers\\", '', static::class),
// 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
* @param mixed $object
*
* @return bool
*/
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;
}
/**
* 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/Beta/Base.php b/src/app/Handlers/Beta/Base.php
index bbca315e..32b28e20 100644
--- a/src/app/Handlers/Beta/Base.php
+++ b/src/app/Handlers/Beta/Base.php
@@ -1,70 +1,70 @@
<?php
namespace App\Handlers\Beta;
class Base extends \App\Handlers\Base
{
/**
* Check if the SKU is available to the user/domain.
*
* @param \App\Sku $sku The SKU object
* @param \App\User|\App\Domain $object The user or domain object
*
* @return bool
*/
public static function isAvailable(\App\Sku $sku, $object): bool
{
// These SKUs must be:
// 1) already assigned or
// 2) active and a 'beta' entitlement must exist.
if (!$object instanceof \App\User) {
return false;
}
if ($sku->active) {
return $object->hasSku('beta');
} else {
if ($object->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'];
+ $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/Distlist.php b/src/app/Handlers/Beta/Distlists.php
similarity index 95%
rename from src/app/Handlers/Distlist.php
rename to src/app/Handlers/Beta/Distlists.php
index d4d19ab9..014e9b67 100644
--- a/src/app/Handlers/Distlist.php
+++ b/src/app/Handlers/Beta/Distlists.php
@@ -1,49 +1,49 @@
<?php
-namespace App\Handlers;
+namespace App\Handlers\Beta;
-class Distlist extends Beta\Base
+class Distlists extends Base
{
/**
* The entitleable class for this handler.
*
* @return string
*/
public static function entitleableClass(): string
{
return \App\User::class;
}
/**
* Check if the SKU is available to the user/domain.
*
* @param \App\Sku $sku The SKU object
* @param \App\User|\App\Domain $object The user or domain object
*
* @return bool
*/
public static function isAvailable(\App\Sku $sku, $object): bool
{
// This SKU must be:
// - already assigned, or active and a 'beta' entitlement must exist
// - and this is a group account owner (custom domain)
if (parent::isAvailable($sku, $object)) {
return $object->wallet()->entitlements()
->where('entitleable_type', \App\Domain::class)->count() > 0;
}
return false;
}
/**
* 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 10;
}
}
diff --git a/src/app/Handlers/Meet.php b/src/app/Handlers/Meet.php
index 277f1138..a30f142a 100644
--- a/src/app/Handlers/Meet.php
+++ b/src/app/Handlers/Meet.php
@@ -1,43 +1,43 @@
<?php
namespace App\Handlers;
class Meet extends Base
{
/**
* The entitleable class for this handler.
*
* @return string
*/
public static function entitleableClass(): string
{
return \App\User::class;
}
/**
* 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'] = ['groupware'];
+ $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 50;
}
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
index 6fb39771..ddd66ce5 100644
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -1,728 +1,728 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\RelationController;
use App\Domain;
use App\Group;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\Sku;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class UsersController extends RelationController
{
/** @const array List of user setting keys available for modification in UI */
public const USER_SETTINGS = [
'billing_address',
'country',
'currency',
'external_email',
'first_name',
'last_name',
'organization',
'phone',
];
/**
* On user create it is filled with a user or group object to force-delete
* before the creation of a new user record is possible.
*
* @var \App\User|\App\Group|null
*/
protected $deleteBeforeCreate;
/** @var string Resource localization label */
protected $label = 'user';
/** @var string Resource model name */
protected $model = User::class;
/** @var array Common object properties in the API response */
protected $objectProps = ['email'];
/**
* Listing of users.
*
* The user-entitlements billed to the current user wallet(s)
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->guard()->user();
$search = trim(request()->input('search'));
$page = intval(request()->input('page')) ?: 1;
$pageSize = 20;
$hasMore = false;
$result = $user->users();
// Search by user email, alias or name
if (strlen($search) > 0) {
// thanks to cloning we skip some extra queries in $user->users()
$allUsers1 = clone $result;
$allUsers2 = clone $result;
$result->whereLike('email', $search)
->union(
$allUsers1->join('user_aliases', 'users.id', '=', 'user_aliases.user_id')
->whereLike('alias', $search)
)
->union(
$allUsers2->join('user_settings', 'users.id', '=', 'user_settings.user_id')
->whereLike('value', $search)
->whereIn('key', ['first_name', 'last_name'])
);
}
$result = $result->orderBy('email')
->limit($pageSize + 1)
->offset($pageSize * ($page - 1))
->get();
if (count($result) > $pageSize) {
$result->pop();
$hasMore = true;
}
// Process the result
$result = $result->map(
function ($user) {
return $this->objectToClient($user);
}
);
$result = [
'list' => $result,
'count' => count($result),
'hasMore' => $hasMore,
];
return response()->json($result);
}
/**
* Display information on the user account specified by $id.
*
* @param string $id The account to show information for.
*
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
$user = User::find($id);
if (!$this->checkTenant($user)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canRead($user)) {
return $this->errorResponse(403);
}
$response = $this->userResponse($user);
$response['skus'] = \App\Entitlement::objectEntitlementsSummary($user);
$response['config'] = $user->getConfig();
return response()->json($response);
}
/**
* User status (extended) information
*
* @param \App\User $user User object
*
* @return array Status information
*/
public static function statusInfo($user): array
{
$process = self::processStateInfo(
$user,
[
'user-new' => true,
'user-ldap-ready' => $user->isLdapReady(),
'user-imap-ready' => $user->isImapReady(),
]
);
// Check if the user is a controller of his wallet
$isController = $user->canDelete($user);
$hasCustomDomain = $user->wallet()->entitlements()
->where('entitleable_type', Domain::class)
->count() > 0;
// Get user's entitlements titles
$skus = $user->entitlements()->select('skus.title')
->join('skus', 'skus.id', '=', 'entitlements.sku_id')
->get()
->pluck('title')
->sort()
->unique()
->values()
->all();
$result = [
'skus' => $skus,
// TODO: This will change when we enable all users to create domains
'enableDomains' => $isController && $hasCustomDomain,
// TODO: Make 'enableDistlists' working for wallet controllers that aren't account owners
- 'enableDistlists' => $isController && $hasCustomDomain && in_array('distlist', $skus),
+ 'enableDistlists' => $isController && $hasCustomDomain && in_array('beta-distlists', $skus),
// TODO: Make 'enableFolders' working for wallet controllers that aren't account owners
'enableFolders' => $isController && $hasCustomDomain && in_array('beta-shared-folders', $skus),
// TODO: Make 'enableResources' working for wallet controllers that aren't account owners
'enableResources' => $isController && $hasCustomDomain && in_array('beta-resources', $skus),
'enableUsers' => $isController,
'enableWallets' => $isController,
];
return array_merge($process, $result);
}
/**
* Create a new user record.
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function store(Request $request)
{
$current_user = $this->guard()->user();
$owner = $current_user->wallet()->owner;
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
}
$this->deleteBeforeCreate = null;
if ($error_response = $this->validateUserRequest($request, null, $settings)) {
return $error_response;
}
if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) {
$errors = ['package' => \trans('validation.packagerequired')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
if ($package->isDomain()) {
$errors = ['package' => \trans('validation.packageinvalid')];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
DB::beginTransaction();
// @phpstan-ignore-next-line
if ($this->deleteBeforeCreate) {
$this->deleteBeforeCreate->forceDelete();
}
// Create user record
$user = User::create([
'email' => $request->email,
'password' => $request->password,
]);
$owner->assignPackage($package, $user);
if (!empty($settings)) {
$user->setSettings($settings);
}
if (!empty($request->aliases)) {
$user->setAliases($request->aliases);
}
DB::commit();
return response()->json([
'status' => 'success',
'message' => \trans('app.user-create-success'),
]);
}
/**
* Update user data.
*
* @param \Illuminate\Http\Request $request The API request.
* @param string $id User identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function update(Request $request, $id)
{
$user = User::withEnvTenantContext()->find($id);
if (empty($user)) {
return $this->errorResponse(404);
}
$current_user = $this->guard()->user();
// TODO: Decide what attributes a user can change on his own profile
if (!$current_user->canUpdate($user)) {
return $this->errorResponse(403);
}
if ($error_response = $this->validateUserRequest($request, $user, $settings)) {
return $error_response;
}
// Entitlements, only controller can do that
if ($request->skus !== null && !$current_user->canDelete($user)) {
return $this->errorResponse(422, "You have no permission to change entitlements");
}
DB::beginTransaction();
$this->updateEntitlements($user, $request->skus);
if (!empty($settings)) {
$user->setSettings($settings);
}
if (!empty($request->password)) {
$user->password = $request->password;
$user->save();
}
if (isset($request->aliases)) {
$user->setAliases($request->aliases);
}
// TODO: Make sure that UserUpdate job is created in case of entitlements update
// and no password change. So, for example quota change is applied to LDAP
// TODO: Review use of $user->save() in the above context
DB::commit();
$response = [
'status' => 'success',
'message' => \trans('app.user-update-success'),
];
// For self-update refresh the statusInfo in the UI
if ($user->id == $current_user->id) {
$response['statusInfo'] = self::statusInfo($user);
}
return response()->json($response);
}
/**
* Update user entitlements.
*
* @param \App\User $user The user
* @param array $rSkus List of SKU IDs requested for the user in the form [id=>qty]
*/
protected function updateEntitlements(User $user, $rSkus)
{
if (!is_array($rSkus)) {
return;
}
// list of skus, [id=>obj]
$skus = Sku::withEnvTenantContext()->get()->mapWithKeys(
function ($sku) {
return [$sku->id => $sku];
}
);
// existing entitlement's SKUs
$eSkus = [];
$user->entitlements()->groupBy('sku_id')
->selectRaw('count(*) as total, sku_id')->each(
function ($e) use (&$eSkus) {
$eSkus[$e->sku_id] = $e->total;
}
);
foreach ($skus as $skuID => $sku) {
$e = array_key_exists($skuID, $eSkus) ? $eSkus[$skuID] : 0;
$r = array_key_exists($skuID, $rSkus) ? $rSkus[$skuID] : 0;
if ($sku->handler_class == \App\Handlers\Mailbox::class) {
if ($r != 1) {
throw new \Exception("Invalid quantity of mailboxes");
}
}
if ($e > $r) {
// remove those entitled more than existing
$user->removeSku($sku, ($e - $r));
} elseif ($e < $r) {
// add those requested more than entitled
$user->assignSku($sku, ($r - $e));
}
}
}
/**
* Create a response data array for specified user.
*
* @param \App\User $user User object
*
* @return array Response data
*/
public static function userResponse(User $user): array
{
$response = array_merge($user->toArray(), self::objectState($user));
// Settings
$response['settings'] = [];
foreach ($user->settings()->whereIn('key', self::USER_SETTINGS)->get() as $item) {
$response['settings'][$item->key] = $item->value;
}
// Aliases
$response['aliases'] = [];
foreach ($user->aliases as $item) {
$response['aliases'][] = $item->alias;
}
// Status info
$response['statusInfo'] = self::statusInfo($user);
// Add more info to the wallet object output
$map_func = function ($wallet) use ($user) {
$result = $wallet->toArray();
if ($wallet->discount) {
$result['discount'] = $wallet->discount->discount;
$result['discount_description'] = $wallet->discount->description;
}
if ($wallet->user_id != $user->id) {
$result['user_email'] = $wallet->owner->email;
}
$provider = \App\Providers\PaymentProvider::factory($wallet);
$result['provider'] = $provider->name();
return $result;
};
// Information about wallets and accounts for access checks
$response['wallets'] = $user->wallets->map($map_func)->toArray();
$response['accounts'] = $user->accounts->map($map_func)->toArray();
$response['wallet'] = $map_func($user->wallet());
return $response;
}
/**
* Prepare user statuses for the UI
*
* @param \App\User $user User object
*
* @return array Statuses array
*/
protected static function objectState($user): array
{
return [
'isImapReady' => $user->isImapReady(),
'isLdapReady' => $user->isLdapReady(),
'isSuspended' => $user->isSuspended(),
'isActive' => $user->isActive(),
'isDeleted' => $user->isDeleted() || $user->trashed(),
];
}
/**
* Validate user input
*
* @param \Illuminate\Http\Request $request The API request.
* @param \App\User|null $user User identifier
* @param array $settings User settings (from the request)
*
* @return \Illuminate\Http\JsonResponse|null The error response on error
*/
protected function validateUserRequest(Request $request, $user, &$settings = [])
{
$rules = [
'external_email' => 'nullable|email',
'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/',
'first_name' => 'string|nullable|max:128',
'last_name' => 'string|nullable|max:128',
'organization' => 'string|nullable|max:512',
'billing_address' => 'string|nullable|max:1024',
'country' => 'string|nullable|alpha|size:2',
'currency' => 'string|nullable|alpha|size:3',
'aliases' => 'array|nullable',
];
if (empty($user) || !empty($request->password) || !empty($request->password_confirmation)) {
$rules['password'] = 'required|min:4|max:2048|confirmed';
}
$errors = [];
// Validate input
$v = Validator::make($request->all(), $rules);
if ($v->fails()) {
$errors = $v->errors()->toArray();
}
$controller = $user ? $user->wallet()->owner : $this->guard()->user();
// For new user validate email address
if (empty($user)) {
$email = $request->email;
if (empty($email)) {
$errors['email'] = \trans('validation.required', ['attribute' => 'email']);
} elseif ($error = self::validateEmail($email, $controller, $this->deleteBeforeCreate)) {
$errors['email'] = $error;
}
}
// Validate aliases input
if (isset($request->aliases)) {
$aliases = [];
$existing_aliases = $user ? $user->aliases()->get()->pluck('alias')->toArray() : [];
foreach ($request->aliases as $idx => $alias) {
if (is_string($alias) && !empty($alias)) {
// Alias cannot be the same as the email address (new user)
if (!empty($email) && Str::lower($alias) == Str::lower($email)) {
continue;
}
// validate new aliases
if (
!in_array($alias, $existing_aliases)
&& ($error = self::validateAlias($alias, $controller))
) {
if (!isset($errors['aliases'])) {
$errors['aliases'] = [];
}
$errors['aliases'][$idx] = $error;
continue;
}
$aliases[] = $alias;
}
}
$request->aliases = $aliases;
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
// Update user settings
$settings = $request->only(array_keys($rules));
unset($settings['password'], $settings['aliases'], $settings['email']);
return null;
}
/**
* Execute (synchronously) specified step in a user setup process.
*
* @param \App\User $user User object
* @param string $step Step identifier (as in self::statusInfo())
*
* @return bool|null True if the execution succeeded, False if not, Null when
* the job has been sent to the worker (result unknown)
*/
public static function execProcessStep(User $user, string $step): ?bool
{
try {
if (strpos($step, 'domain-') === 0) {
list ($local, $domain) = explode('@', $user->email);
$domain = Domain::where('namespace', $domain)->first();
return DomainsController::execProcessStep($domain, $step);
}
switch ($step) {
case 'user-ldap-ready':
// User not in LDAP, create it
$job = new \App\Jobs\User\CreateJob($user->id);
$job->handle();
$user->refresh();
return $user->isLdapReady();
case 'user-imap-ready':
// User not in IMAP? Verify again
// Do it synchronously if the imap admin credentials are available
// otherwise let the worker do the job
if (!\config('imap.admin_password')) {
\App\Jobs\User\VerifyJob::dispatch($user->id);
return null;
}
$job = new \App\Jobs\User\VerifyJob($user->id);
$job->handle();
$user->refresh();
return $user->isImapReady();
}
} catch (\Exception $e) {
\Log::error($e);
}
return false;
}
/**
* Email address validation for use as a user mailbox (login).
*
* @param string $email Email address
* @param \App\User $user The account owner
* @param null|\App\User|\App\Group $deleted Filled with an instance of a deleted user or group
* with the specified email address, if exists
*
* @return ?string Error message on validation error
*/
public static function validateEmail(string $email, \App\User $user, &$deleted = null): ?string
{
$deleted = null;
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
list($login, $domain) = explode('@', Str::lower($email));
if (strlen($login) === 0 || strlen($domain) === 0) {
return \trans('validation.entryinvalid', ['attribute' => 'email']);
}
// Check if domain exists
$domain = Domain::withObjectTenantContext($user)->where('namespace', $domain)->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
// Validate login part alone
$v = Validator::make(
['email' => $login],
['email' => ['required', new UserEmailLocal(!$domain->isPublic())]]
);
if ($v->fails()) {
return $v->errors()->toArray()['email'][0];
}
// Check if it is one of domains available to the user
if (!$user->domains()->where('namespace', $domain->namespace)->exists()) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
// Check if a user with specified address already exists
if ($existing_user = User::emailExists($email, true)) {
// If this is a deleted user in the same custom domain
// we'll force delete him before
if (!$domain->isPublic() && $existing_user->trashed()) {
$deleted = $existing_user;
} else {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
}
// Check if an alias with specified address already exists.
if (User::aliasExists($email)) {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
// Check if a group or resource with specified address already exists
if (
($existing = Group::emailExists($email, true))
|| ($existing = \App\Resource::emailExists($email, true))
) {
// If this is a deleted group/resource in the same custom domain
// we'll force delete it before
if (!$domain->isPublic() && $existing->trashed()) {
$deleted = $existing;
} else {
return \trans('validation.entryexists', ['attribute' => 'email']);
}
}
return null;
}
/**
* Email address validation for use as an alias.
*
* @param string $email Email address
* @param \App\User $user The account owner
*
* @return ?string Error message on validation error
*/
public static function validateAlias(string $email, \App\User $user): ?string
{
if (strpos($email, '@') === false) {
return \trans('validation.entryinvalid', ['attribute' => 'alias']);
}
list($login, $domain) = explode('@', Str::lower($email));
if (strlen($login) === 0 || strlen($domain) === 0) {
return \trans('validation.entryinvalid', ['attribute' => 'alias']);
}
// Check if domain exists
$domain = Domain::withObjectTenantContext($user)->where('namespace', $domain)->first();
if (empty($domain)) {
return \trans('validation.domaininvalid');
}
// Validate login part alone
$v = Validator::make(
['alias' => $login],
['alias' => ['required', new UserEmailLocal(!$domain->isPublic())]]
);
if ($v->fails()) {
return $v->errors()->toArray()['alias'][0];
}
// Check if it is one of domains available to the user
if (!$user->domains()->where('namespace', $domain->namespace)->exists()) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
// Check if a user with specified address already exists
if ($existing_user = User::emailExists($email, true)) {
// Allow an alias in a custom domain to an address that was a user before
if ($domain->isPublic() || !$existing_user->trashed()) {
return \trans('validation.entryexists', ['attribute' => 'alias']);
}
}
// Check if an alias with specified address already exists
if (User::aliasExists($email)) {
// Allow assigning the same alias to a user in the same group account,
// but only for non-public domains
if ($domain->isPublic()) {
return \trans('validation.entryexists', ['attribute' => 'alias']);
}
}
// Check if a group with specified address already exists
if (Group::emailExists($email)) {
return \trans('validation.entryexists', ['attribute' => 'alias']);
}
return null;
}
}
diff --git a/src/database/migrations/2021_12_15_100000_rename_beta_skus.php b/src/database/migrations/2021_12_15_100000_rename_beta_skus.php
new file mode 100644
index 00000000..a4c733d0
--- /dev/null
+++ b/src/database/migrations/2021_12_15_100000_rename_beta_skus.php
@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+// phpcs:ignore
+class RenameBetaSkus extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ \App\Sku::where('title', 'distlist')->get()->each(function ($sku) {
+ $sku->title = 'beta-distlists';
+ $sku->handler_class = 'App\Handlers\Beta\Distlists';
+ $sku->save();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ \App\Sku::where('title', 'beta-distlists')->get()->each(function ($sku) {
+ $sku->title = 'distlist';
+ $sku->handler_class = 'App\Handlers\Distlist';
+ $sku->save();
+ });
+ }
+}
diff --git a/src/database/seeds/local/SkuSeeder.php b/src/database/seeds/local/SkuSeeder.php
index eaa0db48..dd13300f 100644
--- a/src/database/seeds/local/SkuSeeder.php
+++ b/src/database/seeds/local/SkuSeeder.php
@@ -1,364 +1,364 @@
<?php
namespace Database\Seeds\Local;
use App\Sku;
use Illuminate\Database\Seeder;
class SkuSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Sku::create(
[
'title' => 'mailbox',
'name' => 'User Mailbox',
'description' => 'Just a mailbox',
'cost' => 500,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Mailbox',
'active' => true,
]
);
Sku::create(
[
'title' => 'domain',
'name' => 'Hosted Domain',
'description' => 'Somewhere to place a mailbox',
'cost' => 100,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Domain',
'active' => false,
]
);
Sku::create(
[
'title' => 'domain-registration',
'name' => 'Domain Registration',
'description' => 'Register a domain with us',
'cost' => 101,
'period' => 'yearly',
'handler_class' => 'App\Handlers\DomainRegistration',
'active' => false,
]
);
Sku::create(
[
'title' => 'domain-hosting',
'name' => 'External Domain',
'description' => 'Host a domain that is externally registered',
'cost' => 100,
'units_free' => 1,
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainHosting',
'active' => true,
]
);
Sku::create(
[
'title' => 'domain-relay',
'name' => 'Domain Relay',
'description' => 'A domain you host at home, for which we relay email',
'cost' => 103,
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainRelay',
'active' => false,
]
);
Sku::create(
[
'title' => 'storage',
'name' => 'Storage Quota',
'description' => 'Some wiggle room',
'cost' => 25,
'units_free' => 5,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Storage',
'active' => true,
]
);
Sku::create(
[
'title' => 'groupware',
'name' => 'Groupware Features',
'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.',
'cost' => 490,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Groupware',
'active' => true,
]
);
Sku::create(
[
'title' => 'resource',
'name' => 'Resource',
'description' => 'Reservation taker',
'cost' => 101,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Resource',
'active' => true,
]
);
Sku::create(
[
'title' => 'shared-folder',
'name' => 'Shared Folder',
'description' => 'A shared folder',
'cost' => 89,
'period' => 'monthly',
'handler_class' => 'App\Handlers\SharedFolder',
'active' => true,
]
);
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' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Activesync',
'active' => true,
]
);
// Check existence because migration might have added this already
$sku = Sku::where(['title' => 'beta', 'tenant_id' => \config('app.tenant_id')])->first();
if (!$sku) {
Sku::create(
[
'title' => 'beta',
'name' => 'Private Beta (invitation only)',
'description' => 'Access to the private beta program subscriptions',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta',
'active' => false,
]
);
}
// Check existence because migration might have added this already
$sku = Sku::where(['title' => 'meet', 'tenant_id' => \config('app.tenant_id')])->first();
if (!$sku) {
Sku::create(
[
'title' => 'meet',
'name' => 'Voice & Video Conferencing (public beta)',
'description' => 'Video conferencing tool',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Meet',
'active' => true,
]
);
}
// Check existence because migration might have added this already
$sku = Sku::where(['title' => 'group', 'tenant_id' => \config('app.tenant_id')])->first();
if (!$sku) {
Sku::create(
[
'title' => 'group',
'name' => 'Group',
'description' => 'Distribution list',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Group',
'active' => true,
]
);
}
// Check existence because migration might have added this already
- $sku = Sku::where(['title' => 'distlist', 'tenant_id' => \config('app.tenant_id')])->first();
+ $sku = Sku::where(['title' => 'beta-distlists', 'tenant_id' => \config('app.tenant_id')])->first();
if (!$sku) {
Sku::create(
[
- 'title' => 'distlist',
+ 'title' => 'beta-distlists',
'name' => 'Distribution lists',
'description' => 'Access to mail distribution lists',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Distlist',
+ 'handler_class' => 'App\Handlers\Beta\Distlists',
'active' => true,
]
);
}
// Check existence because migration might have added this already
$sku = Sku::where(['title' => 'beta-resources', 'tenant_id' => \config('app.tenant_id')])->first();
if (!$sku) {
Sku::create([
'title' => 'beta-resources',
'name' => 'Calendaring resources',
'description' => 'Access to calendaring resources',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\Resources',
'active' => true,
]);
}
// Check existence because migration might have added this already
$sku = Sku::where(['title' => 'beta-shared-folders', 'tenant_id' => \config('app.tenant_id')])->first();
if (!$sku) {
Sku::create([
'title' => 'beta-shared-folders',
'name' => 'Shared folders',
'description' => 'Access to shared folders',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\SharedFolders',
'active' => true,
]);
}
// for tenants that are not the configured tenant id
$tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get();
foreach ($tenants as $tenant) {
$sku = Sku::create(
[
'title' => 'mailbox',
'name' => 'User Mailbox',
'description' => 'Just a mailbox',
'cost' => 500,
'fee' => 333,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Mailbox',
'active' => true,
]
);
$sku->tenant_id = $tenant->id;
$sku->save();
$sku = Sku::create(
[
'title' => 'storage',
'name' => 'Storage Quota',
'description' => 'Some wiggle room',
'cost' => 25,
'fee' => 16,
'units_free' => 5,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Storage',
'active' => true,
]
);
$sku->tenant_id = $tenant->id;
$sku->save();
$sku = Sku::create(
[
'title' => 'domain-hosting',
'name' => 'External Domain',
'description' => 'Host a domain that is externally registered',
'cost' => 100,
'fee' => 66,
'units_free' => 1,
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainHosting',
'active' => true,
]
);
$sku->tenant_id = $tenant->id;
$sku->save();
$sku = Sku::create(
[
'title' => 'groupware',
'name' => 'Groupware Features',
'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.',
'cost' => 490,
'fee' => 327,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Groupware',
'active' => true,
]
);
$sku->tenant_id = $tenant->id;
$sku->save();
$sku = 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->tenant_id = $tenant->id;
$sku->save();
$sku = Sku::create(
[
'title' => 'activesync',
'name' => 'Activesync',
'description' => 'Mobile synchronization',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Activesync',
'active' => true,
]
);
$sku->tenant_id = $tenant->id;
$sku->save();
}
}
}
diff --git a/src/database/seeds/production/SkuSeeder.php b/src/database/seeds/production/SkuSeeder.php
index 107b76b5..b7f4deee 100644
--- a/src/database/seeds/production/SkuSeeder.php
+++ b/src/database/seeds/production/SkuSeeder.php
@@ -1,245 +1,245 @@
<?php
namespace Database\Seeds\Production;
use App\Sku;
use Illuminate\Database\Seeder;
class SkuSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Sku::create(
[
'title' => 'mailbox',
'name' => 'User Mailbox',
'description' => 'Just a mailbox',
'cost' => 444,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Mailbox',
'active' => true,
]
);
Sku::create(
[
'title' => 'domain',
'name' => 'Hosted Domain',
'description' => 'Somewhere to place a mailbox',
'cost' => 100,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Domain',
'active' => false,
]
);
Sku::create(
[
'title' => 'domain-registration',
'name' => 'Domain Registration',
'description' => 'Register a domain with us',
'cost' => 101,
'period' => 'yearly',
'handler_class' => 'App\Handlers\DomainRegistration',
'active' => false,
]
);
Sku::create(
[
'title' => 'domain-hosting',
'name' => 'External Domain',
'description' => 'Host a domain that is externally registered',
'cost' => 100,
'units_free' => 1,
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainHosting',
'active' => true,
]
);
Sku::create(
[
'title' => 'domain-relay',
'name' => 'Domain Relay',
'description' => 'A domain you host at home, for which we relay email',
'cost' => 103,
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainRelay',
'active' => false,
]
);
Sku::create(
[
'title' => 'storage',
'name' => 'Storage Quota',
'description' => 'Some wiggle room',
'cost' => 50,
'units_free' => 2,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Storage',
'active' => true,
]
);
Sku::create(
[
'title' => 'groupware',
'name' => 'Groupware Features',
'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.',
'cost' => 555,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Groupware',
'active' => true,
]
);
Sku::create(
[
'title' => 'resource',
'name' => 'Resource',
'description' => 'Reservation taker',
'cost' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Resource',
'active' => true,
]
);
Sku::create(
[
'title' => 'shared-folder',
'name' => 'Shared Folder',
'description' => 'A shared folder',
'cost' => 89,
'period' => 'monthly',
'handler_class' => 'App\Handlers\SharedFolder',
'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,
]
);
// Check existence because migration might have added this already
if (!Sku::where('title', 'beta')->first()) {
Sku::create(
[
'title' => 'beta',
'name' => 'Private Beta (invitation only)',
'description' => 'Access to the private beta program subscriptions',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta',
'active' => false,
]
);
}
// Check existence because migration might have added this already
if (!Sku::where('title', 'meet')->first()) {
Sku::create(
[
'title' => 'meet',
'name' => 'Voice & Video Conferencing (public beta)',
'description' => 'Video conferencing tool',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Meet',
'active' => true,
]
);
}
// Check existence because migration might have added this already
if (!Sku::where('title', 'group')->first()) {
Sku::create(
[
'title' => 'group',
'name' => 'Group',
'description' => 'Distribution list',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Group',
'active' => true,
]
);
}
// Check existence because migration might have added this already
- if (!Sku::where('title', 'distlist')->first()) {
+ if (!Sku::where('title', 'beta-distlists')->first()) {
Sku::create([
- 'title' => 'distlist',
+ 'title' => 'beta-distlists',
'name' => 'Distribution lists',
'description' => 'Access to mail distribution lists',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Distlist',
+ 'handler_class' => 'App\Handlers\Beta\Distlists',
'active' => true,
]);
}
// Check existence because migration might have added this already
if (!Sku::where('title', 'beta-resources')->first()) {
Sku::create([
'title' => 'beta-resources',
'name' => 'Calendaring resources',
'description' => 'Access to calendaring resources',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\Resources',
'active' => true,
]);
}
// Check existence because migration might have added this already
if (!Sku::where('title', 'beta-shared-folders')->first()) {
Sku::create([
'title' => 'beta-shared-folders',
'name' => 'Shared folders',
'description' => 'Access to shared folders',
'cost' => 0,
'units_free' => 0,
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\SharedFolders',
'active' => true,
]);
}
}
}
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
index 98612cad..d67b7605 100644
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -1,842 +1,842 @@
<template>
<div class="container">
<div class="card" id="user-info">
<div class="card-body">
<h1 class="card-title">{{ user.email }}</h1>
<div class="card-text">
<form class="read-only short">
<div v-if="user.wallet.user_id != user.id" class="row plaintext">
<label for="manager" class="col-sm-4 col-form-label">{{ $t('user.managed-by') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="manager">
<router-link :to="{ path: '/user/' + user.wallet.user_id }">{{ user.wallet.user_email }}</router-link>
</span>
</div>
</div>
<div class="row plaintext">
<label for="userid" class="col-sm-4 col-form-label">ID <span class="text-muted">({{ $t('form.created') }})</span></label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="userid">
{{ user.id }} <span class="text-muted">({{ user.created_at }})</span>
</span>
</div>
</div>
<div class="row plaintext">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="status">
<span :class="$root.userStatusClass(user)">{{ $root.userStatusText(user) }}</span>
</span>
</div>
</div>
<div class="row plaintext" v-if="user.first_name">
<label for="first_name" class="col-sm-4 col-form-label">{{ $t('form.firstname') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="first_name">{{ user.first_name }}</span>
</div>
</div>
<div class="row plaintext" v-if="user.last_name">
<label for="last_name" class="col-sm-4 col-form-label">{{ $t('form.lastname') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="last_name">{{ user.last_name }}</span>
</div>
</div>
<div class="row plaintext" v-if="user.organization">
<label for="organization" class="col-sm-4 col-form-label">{{ $t('user.org') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="organization">{{ user.organization }}</span>
</div>
</div>
<div class="row plaintext" v-if="user.phone">
<label for="phone" class="col-sm-4 col-form-label">{{ $t('form.phone') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="phone">{{ user.phone }}</span>
</div>
</div>
<div class="row plaintext">
<label for="external_email" class="col-sm-4 col-form-label">{{ $t('user.ext-email') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="external_email">
<a v-if="user.external_email" :href="'mailto:' + user.external_email">{{ user.external_email }}</a>
<button type="button" class="btn btn-secondary btn-sm" @click="emailEdit">{{ $t('btn.edit') }}</button>
</span>
</div>
</div>
<div class="row plaintext" v-if="user.billing_address">
<label for="billing_address" class="col-sm-4 col-form-label">{{ $t('user.address') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" style="white-space:pre" id="billing_address">{{ user.billing_address }}</span>
</div>
</div>
<div class="row plaintext">
<label for="country" class="col-sm-4 col-form-label">{{ $t('user.country') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="country">{{ user.country }}</span>
</div>
</div>
</form>
<div class="mt-2">
<button v-if="!user.isSuspended" id="button-suspend" class="btn btn-warning" type="button" @click="suspendUser">
{{ $t('btn.suspend') }}
</button>
<button v-if="user.isSuspended" id="button-unsuspend" class="btn btn-warning" type="button" @click="unsuspendUser">
{{ $t('btn.unsuspend') }}
</button>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-finances" href="#user-finances" role="tab" aria-controls="user-finances" aria-selected="true">
{{ $t('user.finances') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-aliases" href="#user-aliases" role="tab" aria-controls="user-aliases" aria-selected="false">
{{ $t('user.aliases') }} ({{ user.aliases.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-subscriptions" href="#user-subscriptions" role="tab" aria-controls="user-subscriptions" aria-selected="false">
{{ $t('user.subscriptions') }} ({{ skus.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-domains" href="#user-domains" role="tab" aria-controls="user-domains" aria-selected="false">
{{ $t('user.domains') }} ({{ domains.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-users" href="#user-users" role="tab" aria-controls="user-users" aria-selected="false">
{{ $t('user.users') }} ({{ users.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-distlists" href="#user-distlists" role="tab" aria-controls="user-distlists" aria-selected="false">
{{ $t('user.distlists') }} ({{ distlists.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-resources" href="#user-resources" role="tab" aria-controls="user-resources" aria-selected="false">
{{ $t('user.resources') }} ({{ resources.length }})
</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-shared-folders" href="#user-shared-folders" role="tab" aria-controls="user-shared-folders" aria-selected="false">
{{ $t('dashboard.shared-folders') }} ({{ folders.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">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="user-finances" role="tabpanel" aria-labelledby="tab-finances">
<div class="card-body">
<h2 class="card-title">
{{ $t('wallet.title') }}
<span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(wallet.balance, wallet.currency) }}</strong></span>
</h2>
<div class="card-text">
<form class="read-only short">
<div class="row">
<label class="col-sm-4 col-form-label">{{ $t('user.discount') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="discount">
<span>{{ wallet.discount ? (wallet.discount + '% - ' + wallet.discount_description) : 'none' }}</span>
<button type="button" class="btn btn-secondary btn-sm" @click="discountEdit">{{ $t('btn.edit') }}</button>
</span>
</div>
</div>
<div class="row" v-if="wallet.mandate && wallet.mandate.id">
<label class="col-sm-4 col-form-label">{{ $t('user.auto-payment') }}</label>
<div class="col-sm-8">
<span id="autopayment" :class="'form-control-plaintext' + (wallet.mandateState ? ' text-danger' : '')"
v-html="$t('user.auto-payment-text', {
amount: wallet.mandate.amount + ' ' + wallet.currency,
balance: wallet.mandate.balance + ' ' + wallet.currency,
method: wallet.mandate.method
})"
>
<span v-if="wallet.mandateState">({{ wallet.mandateState }})</span>.
</span>
</div>
</div>
<div class="row" v-if="wallet.providerLink">
<label class="col-sm-4 col-form-label">{{ capitalize(wallet.provider) }} {{ $t('form.id') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" v-html="wallet.providerLink"></span>
</div>
</div>
</form>
<div class="mt-2">
<button id="button-award" class="btn btn-success" type="button" @click="awardDialog">{{ $t('user.add-bonus') }}</button>
<button id="button-penalty" class="btn btn-danger" type="button" @click="penalizeDialog">{{ $t('user.add-penalty') }}</button>
</div>
</div>
<h2 class="card-title mt-4">{{ $t('wallet.transactions') }}</h2>
<transaction-log v-if="wallet.id && !walletReload" class="card-text" :wallet-id="wallet.id" :is-admin="true"></transaction-log>
</div>
</div>
<div class="tab-pane" id="user-aliases" role="tabpanel" aria-labelledby="tab-aliases">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(alias, index) in user.aliases" :id="'alias' + index" :key="index">
<td>{{ alias }}</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td>{{ $t('user.aliases-none') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-subscriptions" role="tabpanel" aria-labelledby="tab-subscriptions">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('user.subscription') }}</th>
<th scope="col">{{ $t('user.price') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(sku, sku_id) in skus" :id="'sku' + sku.id" :key="sku_id">
<td>{{ sku.name }}</td>
- <td>{{ sku.price }}</td>
+ <td class="price">{{ sku.price }}</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('user.subscriptions-none') }}</td>
</tr>
</tfoot>
</table>
<small v-if="discount > 0" class="hint">
<hr class="m-0">
&sup1; {{ $t('user.discount-hint') }}: {{ discount }}% - {{ discount_description }}
</small>
<div class="mt-2">
<button type="button" class="btn btn-danger" id="reset2fa" v-if="has2FA" @click="reset2FADialog">
{{ $t('user.reset-2fa') }}
</button>
<button type="button" class="btn btn-secondary" id="addbetasku" v-if="!hasBeta" @click="addBetaSku">
{{ $t('user.add-beta') }}
</button>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="user-domains" role="tabpanel" aria-labelledby="tab-domains">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('domain.namespace') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="domain in domains" :id="'domain' + domain.id" :key="domain.id" @click="$root.clickRecord">
<td>
<svg-icon icon="globe" :class="$root.domainStatusClass(domain)" :title="$root.domainStatusText(domain)"></svg-icon>
<router-link :to="{ path: '/domain/' + domain.id }">{{ domain.namespace }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td>{{ $t('user.domains-none') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-users" role="tabpanel" aria-labelledby="tab-users">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('form.primary-email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in users" :id="'user' + item.id" :key="item.id" @click="$root.clickRecord">
<td>
<svg-icon icon="user" :class="$root.userStatusClass(item)" :title="$root.userStatusText(item)"></svg-icon>
<router-link v-if="item.id != user.id" :to="{ path: '/user/' + item.id }">{{ item.email }}</router-link>
<span v-else>{{ item.email }}</span>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td>{{ $t('user.users-none') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-distlists" role="tabpanel" aria-labelledby="tab-distlists">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('distlist.name') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="list in distlists" :key="list.id" @click="$root.clickRecord">
<td>
<svg-icon icon="users" :class="$root.distlistStatusClass(list)" :title="$root.distlistStatusText(list)"></svg-icon>
<router-link :to="{ path: '/distlist/' + list.id }">{{ list.name }}</router-link>
</td>
<td>
<router-link :to="{ path: '/distlist/' + list.id }">{{ list.email }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('distlist.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-resources" role="tabpanel" aria-labelledby="tab-resources">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="resource in resources" :key="resource.id" @click="$root.clickRecord">
<td>
<svg-icon icon="cog" :class="$root.resourceStatusClass(resource)" :title="$root.resourceStatusText(resource)"></svg-icon>
<router-link :to="{ path: '/resource/' + resource.id }">{{ resource.name }}</router-link>
</td>
<td>
<router-link :to="{ path: '/resource/' + resource.id }">{{ resource.email }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('resource.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="tab-pane" id="user-shared-folders" role="tabpanel" aria-labelledby="tab-shared-folders">
<div class="card-body">
<div class="card-text">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.type') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="folder in folders" :key="folder.id" @click="$root.clickRecord">
<td>
<svg-icon icon="folder-open" :class="$root.folderStatusClass(folder)" :title="$root.folderStatusText(folder)"></svg-icon>
<router-link :to="{ path: '/shared-folder/' + folder.id }">{{ folder.name }}</router-link>
</td>
<td>{{ $t('shf.type-' + folder.type) }}</td>
<td><router-link :to="{ path: '/shared-folder/' + folder.id }">{{ folder.email }}</router-link></td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="3">{{ $t('shf.list-empty') }}</td>
</tr>
</tfoot>
</table>
</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="row plaintext">
<label for="greylist_enabled" class="col-sm-4 col-form-label">{{ $t('user.greylisting') }}</label>
<div class="col-sm-8">
<span class="form-control-plaintext" id="greylist_enabled">
<span v-if="user.config.greylist_enabled" class="text-success">{{ $t('form.enabled') }}</span>
<span v-else class="text-danger">{{ $t('form.disabled') }}</span>
</span>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="discount-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.discount-title') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>
<select v-model="wallet.discount_id" class="form-select">
<option value="">- {{ $t('form.none') }} -</option>
<option v-for="item in discounts" :value="item.id" :key="item.id">{{ item.label }}</option>
</select>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitDiscount()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
</div>
</div>
</div>
</div>
<div id="email-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.ext-email') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>
<input v-model="external_email" name="external_email" class="form-control">
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitEmail()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
</div>
</div>
</div>
</div>
<div id="oneoff-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t(oneoff_negative ? 'user.add-penalty-title' : 'user.add-bonus-title') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<form data-validation-prefix="oneoff_">
<div class="row mb-3">
<label for="oneoff_amount" class="col-form-label">{{ $t('form.amount') }}</label>
<div class="input-group">
<input type="text" class="form-control" id="oneoff_amount" v-model="oneoff_amount" required>
<span class="input-group-text">{{ wallet.currency }}</span>
</div>
</div>
<div class="row">
<label for="oneoff_description" class="col-form-label">{{ $t('form.description') }}</label>
<input class="form-control" id="oneoff_description" v-model="oneoff_description" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-primary modal-action" @click="submitOneOff()">
<svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}
</button>
</div>
</div>
</div>
</div>
<div id="reset-2fa-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ $t('user.reset-2fa-title') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('btn.close')"></button>
</div>
<div class="modal-body">
<p>{{ $t('user.2fa-hint1') }}</p>
<p>{{ $t('user.2fa-hint2') }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary modal-cancel" data-bs-dismiss="modal">{{ $t('btn.cancel') }}</button>
<button type="button" class="btn btn-danger modal-action" @click="reset2FA()">{{ $t('btn.reset') }}</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from 'bootstrap'
import TransactionLog from '../Widgets/TransactionLog'
export default {
components: {
TransactionLog
},
beforeRouteUpdate (to, from, next) {
// An event called when the route that renders this component has changed,
// but this component is reused in the new route.
// Required to handle links from /user/XXX to /user/YYY
next()
this.$parent.routerReload()
},
data() {
return {
oneoff_amount: '',
oneoff_description: '',
oneoff_negative: false,
discount: 0,
discount_description: '',
discounts: [],
external_email: '',
folders: [],
has2FA: false,
hasBeta: false,
wallet: {},
walletReload: false,
distlists: [],
domains: [],
resources: [],
skus: [],
sku2FA: null,
users: [],
user: {
aliases: [],
config: {},
wallet: {},
skus: {},
}
}
},
created() {
const user_id = this.$route.params.user
this.$root.startLoading()
axios.get('/api/v4/users/' + user_id)
.then(response => {
this.$root.stopLoading()
this.user = response.data
const financesTab = '#user-finances'
const keys = ['first_name', 'last_name', 'external_email', 'billing_address', 'phone', 'organization']
let country = this.user.settings.country
if (country && country in window.config.countries) {
country = window.config.countries[country][1]
}
this.user.country = country
keys.forEach(key => { this.user[key] = this.user.settings[key] })
this.discount = this.user.wallet.discount
this.discount_description = this.user.wallet.discount_description
// TODO: currencies, multi-wallets, accounts
// Get more info about the wallet (e.g. payment provider related)
this.$root.addLoader(financesTab)
axios.get('/api/v4/wallets/' + this.user.wallets[0].id)
.then(response => {
this.$root.removeLoader(financesTab)
this.wallet = response.data
this.setMandateState()
})
.catch(error => {
this.$root.removeLoader(financesTab)
})
// Create subscriptions list
axios.get('/api/v4/users/' + user_id + '/skus')
.then(response => {
// "merge" SKUs with user entitlement-SKUs
response.data.forEach(sku => {
const userSku = this.user.skus[sku.id]
if (userSku) {
let cost = userSku.costs.reduce((sum, current) => sum + current)
let item = {
id: sku.id,
name: sku.name,
cost: cost,
price: this.$root.priceLabel(cost, this.discount)
}
if (sku.range) {
item.name += ' ' + userSku.count + ' ' + sku.range.unit
}
this.skus.push(item)
- if (sku.handler == 'auth2f') {
+ if (sku.handler == 'Auth2F') {
this.has2FA = true
this.sku2FA = sku.id
- } else if (sku.handler == 'beta') {
+ } else if (sku.handler == 'Beta') {
this.hasBeta = true
}
}
})
})
// Fetch users
// TODO: Multiple wallets
axios.get('/api/v4/users?owner=' + user_id)
.then(response => {
this.users = response.data.list;
})
// Fetch domains
axios.get('/api/v4/domains?owner=' + user_id)
.then(response => {
this.domains = response.data.list
})
// Fetch distribution lists
axios.get('/api/v4/groups?owner=' + user_id)
.then(response => {
this.distlists = response.data.list
})
// Fetch resources lists
axios.get('/api/v4/resources?owner=' + user_id)
.then(response => {
this.resources = response.data.list
})
// Fetch shared folders lists
axios.get('/api/v4/shared-folders?owner=' + user_id)
.then(response => {
this.folders = response.data.list
})
})
.catch(this.$root.errorHandler)
},
mounted() {
$(this.$el).find('ul.nav-tabs a').on('click', this.$root.tab)
},
methods: {
addBetaSku() {
axios.post('/api/v4/users/' + this.user.id + '/skus/beta')
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.hasBeta = true
const sku = response.data.sku
this.skus.push({
id: sku.id,
name: sku.name,
cost: sku.cost,
price: this.$root.priceLabel(sku.cost, this.discount)
})
}
})
},
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
awardDialog() {
this.oneOffDialog(false)
},
discountEdit() {
if (!this.discount_dialog) {
const dialog = $('#discount-dialog')[0]
dialog.addEventListener('shown.bs.modal', e => {
$(dialog).find('select').focus()
// Note: Vue v-model is strict, convert null to a string
this.wallet.discount_id = this.wallet_discount_id || ''
})
this.discount_dialog = new Modal(dialog)
}
this.discount_dialog.show()
if (!this.discounts.length) {
// Fetch discounts
axios.get('/api/v4/users/' + this.user.id + '/discounts')
.then(response => {
this.discounts = response.data.list
})
}
},
emailEdit() {
this.external_email = this.user.external_email
this.$root.clearFormValidation($('#email-dialog'))
if (!this.email_dialog) {
const dialog = $('#email-dialog')[0]
dialog.addEventListener('shown.bs.modal', e => {
$(dialog).find('input').focus()
})
this.email_dialog = new Modal(dialog)
}
this.email_dialog.show()
},
setMandateState() {
let mandate = this.wallet.mandate
if (mandate && mandate.id) {
if (!mandate.isValid) {
this.wallet.mandateState = mandate.isPending ? 'pending' : 'invalid'
} else if (mandate.isDisabled) {
this.wallet.mandateState = 'disabled'
}
}
},
oneOffDialog(negative) {
this.oneoff_negative = negative
if (!this.oneoff_dialog) {
const dialog = $('#oneoff-dialog')[0]
dialog.addEventListener('shown.bs.modal', () => {
this.$root.clearFormValidation(dialog)
$(dialog).find('#oneoff_amount').focus()
})
this.oneoff_dialog = new Modal(dialog)
}
this.oneoff_dialog.show()
},
penalizeDialog() {
this.oneOffDialog(true)
},
reload() {
// this is to reload transaction log
this.walletReload = true
this.$nextTick(() => { this.walletReload = false })
},
reset2FA() {
new Modal('#reset-2fa-dialog').hide()
axios.post('/api/v4/users/' + this.user.id + '/reset2FA')
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.skus = this.skus.filter(sku => sku.id != this.sku2FA)
this.has2FA = false
}
})
},
reset2FADialog() {
new Modal('#reset-2fa-dialog').show()
},
submitDiscount() {
this.discount_dialog.hide()
axios.put('/api/v4/wallets/' + this.user.wallets[0].id, { discount: this.wallet.discount_id })
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.wallet = Object.assign({}, this.wallet, response.data)
// Update prices in Subscriptions tab
if (this.user.wallet.id == response.data.id) {
this.discount = this.wallet.discount
this.discount_description = this.wallet.discount_description
this.skus.forEach(sku => {
sku.price = this.$root.priceLabel(sku.cost, this.discount)
})
}
}
})
},
submitEmail() {
axios.put('/api/v4/users/' + this.user.id, { external_email: this.external_email })
.then(response => {
if (response.data.status == 'success') {
this.email_dialog.hide()
this.$toast.success(response.data.message)
this.user.external_email = this.external_email
this.external_email = null // required because of Vue
}
})
},
submitOneOff() {
let wallet_id = this.user.wallets[0].id
let post = {
amount: this.oneoff_amount,
description: this.oneoff_description
}
if (this.oneoff_negative && /^\d+(\.?\d+)?$/.test(post.amount)) {
post.amount *= -1
}
this.$root.clearFormValidation('#oneoff-dialog')
axios.post('/api/v4/wallets/' + wallet_id + '/one-off', post)
.then(response => {
if (response.data.status == 'success') {
this.oneoff_dialog.hide()
this.$toast.success(response.data.message)
this.wallet = Object.assign({}, this.wallet, {balance: response.data.balance})
this.oneoff_amount = ''
this.oneoff_description = ''
this.reload()
}
})
},
suspendUser() {
axios.post('/api/v4/users/' + this.user.id + '/suspend', {})
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.user = Object.assign({}, this.user, { isSuspended: true })
}
})
},
unsuspendUser() {
axios.post('/api/v4/users/' + this.user.id + '/unsuspend', {})
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.user = Object.assign({}, this.user, { isSuspended: false })
}
})
}
}
}
</script>
diff --git a/src/resources/vue/Widgets/SubscriptionSelect.vue b/src/resources/vue/Widgets/SubscriptionSelect.vue
index e26fc1cd..5b0c0de6 100644
--- a/src/resources/vue/Widgets/SubscriptionSelect.vue
+++ b/src/resources/vue/Widgets/SubscriptionSelect.vue
@@ -1,208 +1,207 @@
<template>
<div>
<table class="table table-sm form-list">
<thead class="visually-hidden">
<tr>
<th scope="col"></th>
<th scope="col">{{ $t('user.subscription') }}</th>
<th scope="col">{{ $t('user.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 || 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="form-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, discount, currency) }}
</td>
<td class="buttons">
<button v-if="sku.description" type="button" class="btn btn-link btn-lg p-0" v-tooltip="sku.description">
<svg-icon icon="info-circle"></svg-icon>
<span class="visually-hidden">{{ $t('btn.moreinfo') }}</span>
</button>
</td>
</tr>
</tbody>
</table>
<small v-if="discount > 0" class="hint">
<hr class="m-0 mt-1">
&sup1; {{ $t('user.discount-hint') }}: {{ discount }}% - {{ discount_description }}
</small>
</div>
</template>
<script>
export default {
props: {
object: { type: Object, default: () => {} },
readonly: { type: Boolean, default: false },
type: { type: String, default: 'user' }
},
data() {
return {
currency: '',
discount: 0,
discount_description: '',
skus: []
}
},
created() {
// assign currency, discount, discount_description of the current user
this.$root.userWalletProps(this)
if (this.object.wallet) {
this.discount = this.object.wallet.discount
this.discount_description = this.object.wallet.discount_description
}
this.$root.startLoading()
axios.get('/api/v4/' + this.type + 's/' + this.object.id + '/skus')
.then(response => {
this.$root.stopLoading()
if (this.readonly) {
response.data = response.data.filter(sku => { return sku.id in this.object.skus })
}
// "merge" SKUs with user entitlement-SKUs
this.skus = response.data
.map(sku => {
const objSku = this.object.skus[sku.id]
if (objSku) {
sku.enabled = true
sku.skuCost = sku.cost
sku.cost = objSku.costs.reduce((sum, current) => sum + current)
sku.value = objSku.count
sku.costs = objSku.costs
} else if (!sku.readonly) {
sku.enabled = false
}
return sku
})
// Update all range inputs (and price)
this.$nextTick(() => {
$(this.$el).find('input[type=range]').each((idx, elem) => { this.rangeUpdate(elem) })
})
})
.catch(this.$root.errorHandler)
},
methods: {
findSku(id) {
for (let i = 0; i < this.skus.length; i++) {
if (this.skus[i].id == id) {
return this.skus[i];
}
}
},
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 => {
+ (sku.required || []).forEach(requiredHandler => {
this.skus.forEach(item => {
- let checkbox
- if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
- if (!checkbox.checked) {
+ if (item.handler == requiredHandler) {
+ if (!$('#s' + item.id).find('input[type=checkbox]:checked').length) {
required.push(item.name)
}
}
})
})
if (required.length) {
input.checked = false
return alert(this.$t('user.skureq', { sku: sku.name, list: 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 => {
+ (sku.forbidden || []).forEach(forbiddenHandler => {
this.skus.forEach(item => {
let checkbox
- if (item.handler == title && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
+ if (item.handler == forbiddenHandler && (checkbox = $('#s' + item.id).find('input[type=checkbox]')[0])) {
if (input.checked) {
checkbox.checked = false
checkbox.readOnly = true
} else {
checkbox.readOnly = false
}
}
})
})
},
rangeUpdate(e) {
let input = $(e.target || e)
let value = input.val()
let record = input.parents('tr').first()
let sku_id = record.find('input[type=checkbox]').val()
let sku = this.findSku(sku_id)
let existing = sku.costs ? sku.costs.length : 0
let cost
// Calculate cost, considering both existing entitlement cost and sku cost
if (existing) {
cost = sku.costs
.sort((a, b) => a - b) // sort by cost ascending (free units first)
.slice(0, value)
.reduce((sum, current) => sum + current)
if (value > existing) {
cost += sku.skuCost * (value - existing)
}
} else {
cost = sku.cost * (value - sku.units_free)
}
// Update the label
input.prev().text(value + ' ' + sku.range.unit)
// Update the price
record.find('.price').text(this.$root.priceLabel(cost, this.discount, this.currency))
}
}
}
</script>
diff --git a/src/tests/Browser/DistlistTest.php b/src/tests/Browser/DistlistTest.php
index 966a0517..2d316b2b 100644
--- a/src/tests/Browser/DistlistTest.php
+++ b/src/tests/Browser/DistlistTest.php
@@ -1,326 +1,314 @@
<?php
namespace Tests\Browser;
use App\Group;
use App\Sku;
use Tests\Browser;
use Tests\Browser\Components\ListInput;
use Tests\Browser\Components\Status;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\DistlistInfo;
use Tests\Browser\Pages\DistlistList;
use Tests\TestCaseDusk;
class DistlistTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestGroup('group-test@kolab.org');
$this->clearBetaEntitlements();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestGroup('group-test@kolab.org');
$this->clearBetaEntitlements();
parent::tearDown();
}
/**
* Test distlist info page (unauthenticated)
*/
public function testInfoUnauth(): void
{
// Test that the page requires authentication
$this->browse(function (Browser $browser) {
$browser->visit('/distlist/abc')->on(new Home());
});
}
/**
* Test distlist list page (unauthenticated)
*/
public function testListUnauth(): void
{
// Test that the page requires authentication
$this->browse(function (Browser $browser) {
$browser->visit('/distlists')->on(new Home());
});
}
/**
* Test distlist list page
*/
public function testList(): void
{
// Log on the user
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
->on(new Dashboard())
->assertMissing('@links .link-distlists');
});
- // Test that Distribution lists page is not accessible without the 'distlist' entitlement
+ // Test that Distribution lists page is not accessible without the 'beta-distlists' entitlement
$this->browse(function (Browser $browser) {
$browser->visit('/distlists')
->assertErrorPage(403);
});
// Create a single group, add beta+distlist entitlements
$john = $this->getTestUser('john@kolab.org');
- $this->addDistlistEntitlement($john);
+ $this->addBetaEntitlement($john, 'beta-distlists');
$group = $this->getTestGroup('group-test@kolab.org', ['name' => 'Test Group']);
$group->assignToWallet($john->wallets->first());
// Test distribution lists page
$this->browse(function (Browser $browser) {
$browser->visit(new Dashboard())
->assertSeeIn('@links .link-distlists', 'Distribution lists')
->click('@links .link-distlists')
->on(new DistlistList())
->whenAvailable('@table', function (Browser $browser) {
$browser->waitFor('tbody tr')
->assertSeeIn('thead tr th:nth-child(1)', 'Name')
->assertSeeIn('thead tr th:nth-child(2)', 'Email')
->assertElementsCount('tbody tr', 1)
->assertSeeIn('tbody tr:nth-child(1) td:nth-child(1) a', 'Test Group')
->assertText('tbody tr:nth-child(1) td:nth-child(1) svg.text-danger title', 'Not Ready')
->assertSeeIn('tbody tr:nth-child(1) td:nth-child(2) a', 'group-test@kolab.org')
->assertMissing('tfoot');
});
});
}
/**
* Test distlist creation/editing/deleting
*
* @depends testList
*/
public function testCreateUpdateDelete(): void
{
- // Test that the page is not available accessible without the 'distlist' entitlement
+ // Test that the page is not available accessible without the 'beta-distlists' entitlement
$this->browse(function (Browser $browser) {
$browser->visit('/distlist/new')
->assertErrorPage(403);
});
// Add beta+distlist entitlements
$john = $this->getTestUser('john@kolab.org');
- $this->addDistlistEntitlement($john);
+ $this->addBetaEntitlement($john, 'beta-distlists');
$this->browse(function (Browser $browser) {
// Create a group
$browser->visit(new DistlistList())
->assertSeeIn('button.create-list', 'Create list')
->click('button.create-list')
->on(new DistlistInfo())
->assertSeeIn('#distlist-info .card-title', 'New distribution list')
->assertSeeIn('@nav #tab-general', 'General')
->assertMissing('@nav #tab-settings')
->with('@general', function (Browser $browser) {
// Assert form content
$browser->assertMissing('#status')
->assertFocused('#name')
->assertSeeIn('div.row:nth-child(1) label', 'Name')
->assertValue('div.row:nth-child(1) input[type=text]', '')
->assertSeeIn('div.row:nth-child(2) label', 'Email')
->assertValue('div.row:nth-child(2) input[type=text]', '')
->assertSeeIn('div.row:nth-child(3) label', 'Recipients')
->assertVisible('div.row:nth-child(3) .list-input')
->with(new ListInput('#members'), function (Browser $browser) {
$browser->assertListInputValue([])
->assertValue('@input', '');
})
->assertSeeIn('button[type=submit]', 'Submit');
})
// Test error conditions
->type('#name', str_repeat('A', 192))
->type('#email', 'group-test@kolabnow.com')
->click('@general button[type=submit]')
->waitFor('#members + .invalid-feedback')
->assertSeeIn('#email + .invalid-feedback', 'The specified domain is not available.')
->assertSeeIn('#name + .invalid-feedback', 'The name may not be greater than 191 characters.')
->assertSeeIn('#members + .invalid-feedback', 'At least one recipient is required.')
->assertFocused('#name')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
// Test successful group creation
->type('#name', 'Test Group')
->type('#email', 'group-test@kolab.org')
->with(new ListInput('#members'), function (Browser $browser) {
$browser->addListEntry('test1@gmail.com')
->addListEntry('test2@gmail.com');
})
->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list created successfully.')
->on(new DistlistList())
->assertElementsCount('@table tbody tr', 1);
// Test group update
$browser->click('@table tr:nth-child(1) td:first-child a')
->on(new DistlistInfo())
->assertSeeIn('#distlist-info .card-title', 'Distribution list')
->with('@general', function (Browser $browser) {
// Assert form content
$browser->assertFocused('#name')
->assertSeeIn('div.row:nth-child(1) label', 'Status')
->assertSeeIn('div.row:nth-child(1) span.text-danger', 'Not Ready')
->assertSeeIn('div.row:nth-child(2) label', 'Name')
->assertValue('div.row:nth-child(2) input[type=text]', 'Test Group')
->assertSeeIn('div.row:nth-child(3) label', 'Email')
->assertValue('div.row:nth-child(3) input[type=text]:disabled', 'group-test@kolab.org')
->assertSeeIn('div.row:nth-child(4) label', 'Recipients')
->assertVisible('div.row:nth-child(4) .list-input')
->with(new ListInput('#members'), function (Browser $browser) {
$browser->assertListInputValue(['test1@gmail.com', 'test2@gmail.com'])
->assertValue('@input', '');
})
->assertSeeIn('button[type=submit]', 'Submit');
})
// Test error handling
->with(new ListInput('#members'), function (Browser $browser) {
$browser->addListEntry('invalid address');
})
->click('@general button[type=submit]')
->waitFor('#members + .invalid-feedback')
->assertSeeIn('#members + .invalid-feedback', 'The specified email address is invalid.')
->assertVisible('#members .input-group:nth-child(4) input.is-invalid')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
// Test successful update
->with(new ListInput('#members'), function (Browser $browser) {
$browser->removeListEntry(3)->removeListEntry(2);
})
->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list updated successfully.')
->assertMissing('.invalid-feedback')
->on(new DistlistList())
->assertElementsCount('@table tbody tr', 1);
$group = Group::where('email', 'group-test@kolab.org')->first();
$this->assertSame(['test1@gmail.com'], $group->members);
// Test group deletion
$browser->click('@table tr:nth-child(1) td:first-child a')
->on(new DistlistInfo())
->assertSeeIn('button.button-delete', 'Delete list')
->click('button.button-delete')
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list deleted successfully.')
->on(new DistlistList())
->assertElementsCount('@table tbody tr', 0)
->assertVisible('@table tfoot');
$this->assertNull(Group::where('email', 'group-test@kolab.org')->first());
});
}
/**
* Test distribution list status
*
* @depends testList
*/
public function testStatus(): void
{
$john = $this->getTestUser('john@kolab.org');
- $this->addDistlistEntitlement($john);
+ $this->addBetaEntitlement($john, 'beta-distlists');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
$group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE;
$group->save();
$this->assertFalse($group->isLdapReady());
$this->browse(function ($browser) use ($group) {
// Test auto-refresh
$browser->visit('/distlist/' . $group->id)
->on(new DistlistInfo())
->with(new Status(), function ($browser) {
$browser->assertSeeIn('@body', 'We are preparing the distribution list')
->assertProgress(83, 'Creating a distribution list...', 'pending')
->assertMissing('@refresh-button')
->assertMissing('@refresh-text')
->assertMissing('#status-link')
->assertMissing('#status-verify');
});
$group->status |= Group::STATUS_LDAP_READY;
$group->save();
// Test Verify button
$browser->waitUntilMissing('@status', 10);
});
// TODO: Test all group statuses on the list
}
/**
* Test distribution list settings
*/
public function testSettings(): void
{
$john = $this->getTestUser('john@kolab.org');
- $this->addDistlistEntitlement($john);
+ $this->addBetaEntitlement($john, 'beta-distlists');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
$group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE;
$group->save();
$this->browse(function ($browser) use ($group) {
// Test auto-refresh
$browser->visit('/distlist/' . $group->id)
->on(new DistlistInfo())
->assertSeeIn('@nav #tab-general', 'General')
->assertSeeIn('@nav #tab-settings', 'Settings')
->click('@nav #tab-settings')
->with('@settings form', function (Browser $browser) {
// Assert form content
$browser->assertSeeIn('div.row:nth-child(1) label', 'Sender Access List')
->assertVisible('div.row:nth-child(1) .list-input')
->with(new ListInput('#sender-policy'), function (Browser $browser) {
$browser->assertListInputValue([])
->assertValue('@input', '');
})
->assertSeeIn('button[type=submit]', 'Submit');
})
// Test error handling
->with(new ListInput('#sender-policy'), function (Browser $browser) {
$browser->addListEntry('test.com');
})
->click('@settings button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'Distribution list settings updated successfully.')
->assertMissing('.invalid-feedback')
->refresh()
->on(new DistlistInfo())
->click('@nav #tab-settings')
->with('@settings form', function (Browser $browser) {
$browser->with(new ListInput('#sender-policy'), function (Browser $browser) {
$browser->assertListInputValue(['test.com'])
->assertValue('@input', '');
});
});
});
}
-
- /**
- * Register the beta + distlist entitlements for the user
- */
- private function addDistlistEntitlement($user): void
- {
- // Add beta+distlist entitlements
- $beta_sku = Sku::where('title', 'beta')->first();
- $distlist_sku = Sku::where('title', 'distlist')->first();
- $user->assignSku($beta_sku);
- $user->assignSku($distlist_sku);
- }
}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
index 89b22525..2906f1bc 100644
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -1,806 +1,806 @@
<?php
namespace Tests\Browser;
use App\Discount;
use App\Entitlement;
use App\Sku;
use App\User;
use App\UserAlias;
use Tests\Browser;
use Tests\Browser\Components\Dialog;
use Tests\Browser\Components\ListInput;
use Tests\Browser\Components\QuotaInput;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\UserInfo;
use Tests\Browser\Pages\UserList;
use Tests\Browser\Pages\Wallet as WalletPage;
use Tests\TestCaseDusk;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class UsersTest extends TestCaseDusk
{
private $profile = [
'first_name' => 'John',
'last_name' => 'Doe',
'organization' => 'Kolab Developers',
];
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('julia.roberts@kolab.org');
$john = User::where('email', 'john@kolab.org')->first();
$john->setSettings($this->profile);
UserAlias::where('user_id', $john->id)
->where('alias', 'john.test@kolab.org')->delete();
$activesync_sku = Sku::withEnvTenantContext()->where('title', 'activesync')->first();
$storage_sku = Sku::withEnvTenantContext()->where('title', 'storage')->first();
Entitlement::where('entitleable_id', $john->id)->where('sku_id', $activesync_sku->id)->delete();
Entitlement::where('cost', '>=', 5000)->delete();
Entitlement::where('cost', '=', 25)->where('sku_id', $storage_sku->id)->delete();
$wallet = $john->wallets()->first();
$wallet->discount()->dissociate();
$wallet->currency = 'CHF';
$wallet->save();
$this->clearBetaEntitlements();
$this->clearMeetEntitlements();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestUser('julia.roberts@kolab.org');
$john = User::where('email', 'john@kolab.org')->first();
$john->setSettings($this->profile);
UserAlias::where('user_id', $john->id)
->where('alias', 'john.test@kolab.org')->delete();
$activesync_sku = Sku::withEnvTenantContext()->where('title', 'activesync')->first();
$storage_sku = Sku::withEnvTenantContext()->where('title', 'storage')->first();
Entitlement::where('entitleable_id', $john->id)->where('sku_id', $activesync_sku->id)->delete();
Entitlement::where('cost', '>=', 5000)->delete();
Entitlement::where('cost', '=', 25)->where('sku_id', $storage_sku->id)->delete();
$wallet = $john->wallets()->first();
$wallet->discount()->dissociate();
$wallet->save();
$this->clearBetaEntitlements();
$this->clearMeetEntitlements();
parent::tearDown();
}
/**
* Test user account editing page (not profile page)
*/
public function testInfo(): void
{
$this->browse(function (Browser $browser) {
$user = User::where('email', 'john@kolab.org')->first();
// Test that the page requires authentication
$browser->visit('/user/' . $user->id)
->on(new Home())
->submitLogon('john@kolab.org', 'simple123', false)
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
->with('@general', function (Browser $browser) {
// Assert form content
$browser->assertSeeIn('div.row:nth-child(1) label', 'Status')
->assertSeeIn('div.row:nth-child(1) #status', 'Active')
->assertFocused('div.row:nth-child(2) input')
->assertSeeIn('div.row:nth-child(2) label', 'First Name')
->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['first_name'])
->assertSeeIn('div.row:nth-child(3) label', 'Last Name')
->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['last_name'])
->assertSeeIn('div.row:nth-child(4) label', 'Organization')
->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['organization'])
->assertSeeIn('div.row:nth-child(5) label', 'Email')
->assertValue('div.row:nth-child(5) input[type=text]', 'john@kolab.org')
->assertDisabled('div.row:nth-child(5) input[type=text]')
->assertSeeIn('div.row:nth-child(6) label', 'Email Aliases')
->assertVisible('div.row:nth-child(6) .list-input')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertListInputValue(['john.doe@kolab.org'])
->assertValue('@input', '');
})
->assertSeeIn('div.row:nth-child(7) label', 'Password')
->assertValue('div.row:nth-child(7) input[type=password]', '')
->assertSeeIn('div.row:nth-child(8) label', 'Confirm Password')
->assertValue('div.row:nth-child(8) input[type=password]', '')
->assertSeeIn('button[type=submit]', 'Submit')
// Clear some fields and submit
->vueClear('#first_name')
->vueClear('#last_name')
->click('button[type=submit]');
})
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.')
->on(new UserList())
->click('@table tr:nth-child(3) a')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
->with('@general', function (Browser $browser) {
// Test error handling (password)
$browser->type('#password', 'aaaaaa')
->vueClear('#password_confirmation')
->click('button[type=submit]')
->waitFor('#password + .invalid-feedback')
->assertSeeIn('#password + .invalid-feedback', 'The password confirmation does not match.')
->assertFocused('#password')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
// TODO: Test password change
// Test form error handling (aliases)
$browser->vueClear('#password')
->vueClear('#password_confirmation')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->addListEntry('invalid address');
})
->click('button[type=submit]')
->assertToast(Toast::TYPE_ERROR, 'Form validation error');
$browser->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertFormError(2, 'The specified alias is invalid.', false);
});
// Test adding aliases
$browser->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->removeListEntry(2)
->addListEntry('john.test@kolab.org');
})
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
})
->on(new UserList())
->click('@table tr:nth-child(3) a')
->on(new UserInfo());
$john = User::where('email', 'john@kolab.org')->first();
$alias = UserAlias::where('user_id', $john->id)->where('alias', 'john.test@kolab.org')->first();
$this->assertTrue(!empty($alias));
// Test subscriptions
$browser->with('@general', function (Browser $browser) {
$browser->assertSeeIn('div.row:nth-child(9) label', 'Subscriptions')
->assertVisible('@skus.row:nth-child(9)')
->with('@skus', function ($browser) {
$browser->assertElementsCount('tbody tr', 6)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.name', 'User Mailbox')
->assertSeeIn('tbody tr:nth-child(1) td.price', '5,00 CHF/month')
->assertChecked('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',
'Just a mailbox'
)
// 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',
'Some wiggle room'
)
->with(new QuotaInput('tbody tr:nth-child(2) .range-input'), function ($browser) {
$browser->assertQuotaValue(5)->setQuotaValue(6);
})
->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', '4,90 CHF/month')
->assertChecked('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',
'Groupware functions like Calendar, Tasks, Notes, etc.'
)
// ActiveSync SKU
->assertSeeIn('tbody tr:nth-child(4) td.name', 'Activesync')
->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',
'Mobile synchronization'
)
// 2FA SKU
->assertSeeIn('tbody tr:nth-child(5) td.name', '2-Factor Authentication')
->assertSeeIn('tbody tr:nth-child(5) td.price', '0,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',
'Two factor authentication for webmail and administration panel'
)
// Meet SKU
->assertSeeIn('tbody tr:nth-child(6) td.name', 'Voice & Video Conferencing (public beta)')
->assertSeeIn('tbody tr:nth-child(6) td.price', '0,00 CHF/month')
->assertNotChecked('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',
'Video conferencing tool'
)
->click('tbody tr:nth-child(4) td.selection input');
})
->assertMissing('@skus table + .hint')
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
})
->on(new UserList())
->click('@table tr:nth-child(3) a')
->on(new UserInfo());
$expected = ['activesync', 'groupware', 'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage', 'storage'];
$this->assertEntitlements($john, $expected);
// Test subscriptions interaction
$browser->with('@general', 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 'meet', expect an alert
->click('#sku-input-meet')
->assertDialogOpened('Voice & Video Conferencing (public beta) requires Groupware Features.')
->acceptDialog()
->assertNotChecked('#sku-input-meet')
// 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');
});
});
});
}
/**
* Test user settings tab
*
* @depends testInfo
*/
public function testUserSettings(): void
{
$john = $this->getTestUser('john@kolab.org');
$john->setSetting('greylist_enabled', null);
$this->browse(function (Browser $browser) use ($john) {
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
->assertElementsCount('@nav a', 2)
->assertSeeIn('@nav #tab-general', 'General')
->assertSeeIn('@nav #tab-settings', 'Settings')
->click('@nav #tab-settings')
->with('#settings form', function (Browser $browser) {
$browser->assertSeeIn('div.row:nth-child(1) label', 'Greylisting')
->click('div.row:nth-child(1) input[type=checkbox]:checked')
->click('button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User settings updated successfully.');
});
});
$this->assertSame('false', $john->fresh()->getSetting('greylist_enabled'));
}
/**
* Test user adding page
*
* @depends testInfo
*/
public function testNewUser(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new UserList())
->assertSeeIn('button.create-user', 'Create user')
->click('button.create-user')
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'New user account')
->with('@general', function (Browser $browser) {
// Assert form content
$browser->assertFocused('div.row:nth-child(1) input')
->assertSeeIn('div.row:nth-child(1) label', 'First Name')
->assertValue('div.row:nth-child(1) input[type=text]', '')
->assertSeeIn('div.row:nth-child(2) label', 'Last Name')
->assertValue('div.row:nth-child(2) input[type=text]', '')
->assertSeeIn('div.row:nth-child(3) label', 'Organization')
->assertValue('div.row:nth-child(3) input[type=text]', '')
->assertSeeIn('div.row:nth-child(4) label', 'Email')
->assertValue('div.row:nth-child(4) input[type=text]', '')
->assertEnabled('div.row:nth-child(4) input[type=text]')
->assertSeeIn('div.row:nth-child(5) label', 'Email Aliases')
->assertVisible('div.row:nth-child(5) .list-input')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertListInputValue([])
->assertValue('@input', '');
})
->assertSeeIn('div.row:nth-child(6) label', 'Password')
->assertValue('div.row:nth-child(6) input[type=password]', '')
->assertSeeIn('div.row:nth-child(7) label', 'Confirm Password')
->assertValue('div.row:nth-child(7) input[type=password]', '')
->assertSeeIn('div.row:nth-child(8) label', 'Package')
// assert packages list widget, select "Lite Account"
->with('@packages', function ($browser) {
$browser->assertElementsCount('tbody tr', 2)
->assertSeeIn('tbody tr:nth-child(1)', 'Groupware Account')
->assertSeeIn('tbody tr:nth-child(2)', 'Lite Account')
->assertSeeIn('tbody tr:nth-child(1) .price', '9,90 CHF/month')
->assertSeeIn('tbody tr:nth-child(2) .price', '5,00 CHF/month')
->assertChecked('tbody tr:nth-child(1) input')
->click('tbody tr:nth-child(2) input')
->assertNotChecked('tbody tr:nth-child(1) input')
->assertChecked('tbody tr:nth-child(2) input');
})
->assertMissing('@packages table + .hint')
->assertSeeIn('button[type=submit]', 'Submit');
// Test browser-side required fields and error handling
$browser->click('button[type=submit]')
->assertFocused('#email')
->type('#email', 'invalid email')
->click('button[type=submit]')
->assertFocused('#password')
->type('#password', 'simple123')
->click('button[type=submit]')
->assertFocused('#password_confirmation')
->type('#password_confirmation', 'simple')
->click('button[type=submit]')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
->assertSeeIn('#email + .invalid-feedback', 'The specified email is invalid.')
->assertSeeIn('#password + .invalid-feedback', 'The password confirmation does not match.');
});
// Test form error handling (aliases)
$browser->with('@general', function (Browser $browser) {
$browser->type('#email', 'julia.roberts@kolab.org')
->type('#password_confirmation', 'simple123')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->addListEntry('invalid address');
})
->click('button[type=submit]')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertFormError(1, 'The specified alias is invalid.', false);
});
});
// Successful account creation
$browser->with('@general', function (Browser $browser) {
$browser->type('#first_name', 'Julia')
->type('#last_name', 'Roberts')
->type('#organization', 'Test Org')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->removeListEntry(1)
->addListEntry('julia.roberts2@kolab.org');
})
->click('button[type=submit]');
})
->assertToast(Toast::TYPE_SUCCESS, 'User created successfully.')
// check redirection to users list
->on(new UserList())
->whenAvailable('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 5)
->assertSeeIn('tbody tr:nth-child(4) a', 'julia.roberts@kolab.org');
});
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
$alias = UserAlias::where('user_id', $julia->id)->where('alias', 'julia.roberts2@kolab.org')->first();
$this->assertTrue(!empty($alias));
$this->assertEntitlements($julia, ['mailbox', 'storage', 'storage', 'storage', 'storage', 'storage']);
$this->assertSame('Julia', $julia->getSetting('first_name'));
$this->assertSame('Roberts', $julia->getSetting('last_name'));
$this->assertSame('Test Org', $julia->getSetting('organization'));
// Some additional tests for the list input widget
$browser->click('@table tbody tr:nth-child(4) a')
->on(new UserInfo())
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertListInputValue(['julia.roberts2@kolab.org'])
->addListEntry('invalid address')
->type('.input-group:nth-child(2) input', '@kolab.org')
->keys('.input-group:nth-child(2) input', '{enter}');
})
// TODO: Investigate why this click does not work, for now we
// submit the form with Enter key above
//->click('@general button[type=submit]')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
->with(new ListInput('#aliases'), function (Browser $browser) {
$browser->assertVisible('.input-group:nth-child(2) input.is-invalid')
->assertVisible('.input-group:nth-child(3) input.is-invalid')
->type('.input-group:nth-child(2) input', 'julia.roberts3@kolab.org')
->type('.input-group:nth-child(3) input', 'julia.roberts4@kolab.org')
->keys('.input-group:nth-child(3) input', '{enter}');
})
// TODO: Investigate why this click does not work, for now we
// submit the form with Enter key above
//->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
$aliases = $julia->aliases()->orderBy('alias')->get()->pluck('alias')->all();
$this->assertSame(['julia.roberts3@kolab.org', 'julia.roberts4@kolab.org'], $aliases);
});
}
/**
* Test user delete
*
* @depends testNewUser
*/
public function testDeleteUser(): void
{
// First create a new user
$john = $this->getTestUser('john@kolab.org');
$julia = $this->getTestUser('julia.roberts@kolab.org');
$package_kolab = \App\Package::where('title', 'kolab')->first();
$john->assignPackage($package_kolab, $julia);
// Test deleting non-controller user
$this->browse(function (Browser $browser) use ($julia) {
$browser->visit('/user/' . $julia->id)
->on(new UserInfo())
->assertSeeIn('button.button-delete', 'Delete user')
->click('button.button-delete')
->with(new Dialog('#delete-warning'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Delete julia.roberts@kolab.org')
->assertFocused('@button-cancel')
->assertSeeIn('@button-cancel', 'Cancel')
->assertSeeIn('@button-action', 'Delete')
->click('@button-cancel');
})
->waitUntilMissing('#delete-warning')
->click('button.button-delete')
->with(new Dialog('#delete-warning'), function (Browser $browser) {
$browser->click('@button-action');
})
->waitUntilMissing('#delete-warning')
->assertToast(Toast::TYPE_SUCCESS, 'User deleted successfully.')
->on(new UserList())
->with('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 4)
->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org');
});
$julia = User::where('email', 'julia.roberts@kolab.org')->first();
$this->assertTrue(empty($julia));
});
// Test that non-controller user cannot see/delete himself on the users list
$this->browse(function (Browser $browser) {
$browser->visit('/logout')
->on(new Home())
->submitLogon('jack@kolab.org', 'simple123', true)
->visit('/users')
->assertErrorPage(403);
});
// Test that controller user (Ned) can see all the users
$this->browse(function (Browser $browser) {
$browser->visit('/logout')
->on(new Home())
->submitLogon('ned@kolab.org', 'simple123', true)
->visit(new UserList())
->whenAvailable('@table', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 4);
});
// TODO: Test the delete action in details
});
// TODO: Test what happens with the logged in user session after he's been deleted by another user
}
/**
* Test discounted sku/package prices in the UI
*/
public function testDiscountedPrices(): void
{
// Add 10% discount
$discount = Discount::where('code', 'TEST')->first();
$john = User::where('email', 'john@kolab.org')->first();
$wallet = $john->wallet();
$wallet->discount()->associate($discount);
$wallet->save();
// SKUs on user edit page
$this->browse(function (Browser $browser) {
$browser->visit('/logout')
->on(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
->visit(new UserList())
->waitFor('@table tr:nth-child(2)')
->click('@table tr:nth-child(2) a') // joe@kolab.org
->on(new UserInfo())
->with('@general', function (Browser $browser) {
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
->assertElementsCount('tbody tr', 6)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '4,50 CHF/month¹')
// Storage SKU
->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month¹')
->with($quota_input, function (Browser $browser) {
$browser->setQuotaValue(100);
})
->assertSeeIn('tr:nth-child(2) td.price', '21,37 CHF/month¹')
// groupware SKU
->assertSeeIn('tbody tr:nth-child(3) td.price', '4,41 CHF/month¹')
// ActiveSync SKU
->assertSeeIn('tbody tr:nth-child(4) td.price', '0,00 CHF/month¹')
// 2FA SKU
->assertSeeIn('tbody tr:nth-child(5) td.price', '0,00 CHF/month¹');
})
->assertSeeIn('@skus table + .hint', '¹ applied discount: 10% - Test voucher');
});
});
// Packages on new user page
$this->browse(function (Browser $browser) {
$browser->visit(new UserList())
->click('button.create-user')
->on(new UserInfo())
->with('@general', function (Browser $browser) {
$browser->whenAvailable('@packages', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 2)
->assertSeeIn('tbody tr:nth-child(1) .price', '8,91 CHF/month¹') // Groupware
->assertSeeIn('tbody tr:nth-child(2) .price', '4,50 CHF/month¹'); // Lite
})
->assertSeeIn('@packages table + .hint', '¹ applied discount: 10% - Test voucher');
});
});
// Test using entitlement cost instead of the SKU cost
$this->browse(function (Browser $browser) use ($wallet) {
$joe = User::where('email', 'joe@kolab.org')->first();
$beta_sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$storage_sku = Sku::withEnvTenantContext()->where('title', 'storage')->first();
// Add an extra storage and beta entitlement with different prices
Entitlement::create([
'wallet_id' => $wallet->id,
'sku_id' => $beta_sku->id,
'cost' => 5010,
'entitleable_id' => $joe->id,
'entitleable_type' => User::class
]);
Entitlement::create([
'wallet_id' => $wallet->id,
'sku_id' => $storage_sku->id,
'cost' => 5000,
'entitleable_id' => $joe->id,
'entitleable_type' => User::class
]);
$browser->visit('/user/' . $joe->id)
->on(new UserInfo())
->with('@general', function (Browser $browser) {
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
// Beta SKU
->assertSeeIn('tbody tr:nth-child(7) td.price', '45,09 CHF/month¹')
// Storage SKU
->assertSeeIn('tr:nth-child(2) td.price', '45,00 CHF/month¹')
->with($quota_input, function (Browser $browser) {
$browser->setQuotaValue(7);
})
->assertSeeIn('tr:nth-child(2) td.price', '45,22 CHF/month¹')
->with($quota_input, function (Browser $browser) {
$browser->setQuotaValue(5);
})
->assertSeeIn('tr:nth-child(2) td.price', '0,00 CHF/month¹');
})
->assertSeeIn('@skus table + .hint', '¹ applied discount: 10% - Test voucher');
});
});
}
/**
* Test non-default currency in the UI
*/
public function testCurrency(): void
{
// Add 10% discount
$john = User::where('email', 'john@kolab.org')->first();
$wallet = $john->wallet();
$wallet->balance = -1000;
$wallet->currency = 'EUR';
$wallet->save();
// On Dashboard and the wallet page
$this->browse(function (Browser $browser) {
$browser->visit('/logout')
->on(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
->on(new Dashboard())
->assertSeeIn('@links .link-wallet .badge', '-10,00 €')
->click('@links .link-wallet')
->on(new WalletPage())
->assertSeeIn('#wallet .card-title', 'Account balance -10,00 €');
});
// SKUs on user edit page
$this->browse(function (Browser $browser) {
$browser->visit(new UserList())
->waitFor('@table tr:nth-child(2)')
->click('@table tr:nth-child(2) a') // joe@kolab.org
->on(new UserInfo())
->with('@general', function (Browser $browser) {
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
->assertElementsCount('tbody tr', 6)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '5,00 €/month')
// Storage SKU
->assertSeeIn('tr:nth-child(2) td.price', '0,00 €/month')
->with($quota_input, function (Browser $browser) {
$browser->setQuotaValue(100);
})
->assertSeeIn('tr:nth-child(2) td.price', '23,75 €/month');
});
});
});
// Packages on new user page
$this->browse(function (Browser $browser) {
$browser->visit(new UserList())
->click('button.create-user')
->on(new UserInfo())
->with('@general', function (Browser $browser) {
$browser->whenAvailable('@packages', function (Browser $browser) {
$browser->assertElementsCount('tbody tr', 2)
->assertSeeIn('tbody tr:nth-child(1) .price', '9,90 €/month') // Groupware
->assertSeeIn('tbody tr:nth-child(2) .price', '5,00 €/month'); // Lite
});
});
});
}
/**
* Test beta entitlements
*
* @depends testInfo
*/
public function testBetaEntitlements(): void
{
$this->browse(function (Browser $browser) {
$john = User::where('email', 'john@kolab.org')->first();
$sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$john->assignSku($sku);
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
->with('@skus', function ($browser) {
$browser->assertElementsCount('tbody tr', 10)
// Meet SKU
->assertSeeIn('tbody tr:nth-child(6) td.name', 'Voice & Video Conferencing (public beta)')
->assertSeeIn('tr:nth-child(6) td.price', '0,00 CHF/month')
->assertNotChecked('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',
'Video conferencing tool'
)
// Beta SKU
->assertSeeIn('tbody tr:nth-child(7) td.name', 'Private Beta (invitation only)')
->assertSeeIn('tbody tr:nth-child(7) td.price', '0,00 CHF/month')
->assertChecked('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',
'Access to the private beta program subscriptions'
)
- // Resources SKU
- ->assertSeeIn('tbody tr:nth-child(8) td.name', 'Calendaring resources')
+ // Distlists SKU
+ ->assertSeeIn('tbody tr:nth-child(8) td.name', 'Distribution lists')
->assertSeeIn('tr:nth-child(8) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(8) td.selection input')
->assertEnabled('tbody tr:nth-child(8) td.selection input')
->assertTip(
'tbody tr:nth-child(8) td.buttons button',
- 'Access to calendaring resources'
+ 'Access to mail distribution lists'
)
- // Shared folders SKU
- ->assertSeeIn('tbody tr:nth-child(9) td.name', 'Shared folders')
+ // Resources SKU
+ ->assertSeeIn('tbody tr:nth-child(9) td.name', 'Calendaring resources')
->assertSeeIn('tr:nth-child(9) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(9) td.selection input')
->assertEnabled('tbody tr:nth-child(9) td.selection input')
->assertTip(
'tbody tr:nth-child(9) td.buttons button',
- 'Access to shared folders'
+ 'Access to calendaring resources'
)
- // Distlist SKU
- ->assertSeeIn('tbody tr:nth-child(10) td.name', 'Distribution lists')
+ // Shared folders SKU
+ ->assertSeeIn('tbody tr:nth-child(10) td.name', 'Shared folders')
->assertSeeIn('tr:nth-child(10) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(10) td.selection input')
->assertEnabled('tbody tr:nth-child(10) td.selection input')
->assertTip(
'tbody tr:nth-child(10) td.buttons button',
- 'Access to mail distribution lists'
+ 'Access to shared folders'
)
// Check Distlist, Uncheck Beta, expect Distlist unchecked
- ->click('#sku-input-distlist')
+ ->click('#sku-input-beta-distlists')
->click('#sku-input-beta')
->assertNotChecked('#sku-input-beta')
- ->assertNotChecked('#sku-input-distlist')
- // Click Distlist expect an alert
- ->click('#sku-input-distlist')
+ ->assertNotChecked('#sku-input-beta-distlists')
+ // Click Distlists expect an alert
+ ->click('#sku-input-beta-distlists')
->assertDialogOpened('Distribution lists requires Private Beta (invitation only).')
->acceptDialog()
// Enable Beta and Distlist and submit
->click('#sku-input-beta')
- ->click('#sku-input-distlist');
+ ->click('#sku-input-beta-distlists');
})
->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
$expected = [
'beta',
- 'distlist',
+ 'beta-distlists',
'groupware',
'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage'
];
$this->assertEntitlements($john, $expected);
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
->waitFor('#sku-input-beta')
->click('#sku-input-beta')
->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.');
$expected = [
'groupware',
'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage'
];
$this->assertEntitlements($john, $expected);
});
- // TODO: Test that the Distlist SKU is not available for users that aren't a group account owners
+ // TODO: Test that the Distlists SKU is not available for users that aren't a group account owners
// TODO: Test that entitlements change has immediate effect on the available items in dashboard
// i.e. does not require a page reload nor re-login.
}
}
diff --git a/src/tests/Feature/Controller/Admin/SkusTest.php b/src/tests/Feature/Controller/Admin/SkusTest.php
index 82152625..4d2c618a 100644
--- a/src/tests/Feature/Controller/Admin/SkusTest.php
+++ b/src/tests/Feature/Controller/Admin/SkusTest.php
@@ -1,124 +1,124 @@
<?php
namespace Tests\Feature\Controller\Admin;
use App\Sku;
use Tests\TestCase;
class SkusTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useAdminUrl();
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
$this->clearMeetEntitlements();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
$this->clearMeetEntitlements();
parent::tearDown();
}
/**
* Test fetching SKUs list for a domain (GET /domains/<id>/skus)
*/
public function testDomainSkus(): void
{
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$user = $this->getTestUser('john@kolab.org');
$domain = $this->getTestDOmain('kolab.org');
// Unauth access not allowed
$response = $this->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(401);
// Non-admin access not allowed
$response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(403);
$response = $this->actingAs($admin)->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(1, $json);
// Note: Details are tested where we test API\V4\SkusController
}
/**
* Test fetching SKUs list
*/
public function testIndex(): void
{
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$user = $this->getTestUser('john@kolab.org');
$sku = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
// Unauth access not allowed
$response = $this->get("api/v4/skus");
$response->assertStatus(401);
// User access not allowed on admin API
$response = $this->actingAs($user)->get("api/v4/skus");
$response->assertStatus(403);
$response = $this->actingAs($admin)->get("api/v4/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(13, $json);
$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']);
+ $this->assertSame('Mailbox', $json[0]['handler']);
}
/**
* Test fetching SKUs list for a user (GET /users/<id>/skus)
*/
public function testUserSkus(): void
{
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$user = $this->getTestUser('john@kolab.org');
// Unauth access not allowed
$response = $this->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(401);
// Non-admin access not allowed
$response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(403);
$response = $this->actingAs($admin)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(6, $json);
// Note: Details are tested where we test API\V4\SkusController
}
}
diff --git a/src/tests/Feature/Controller/Reseller/SkusTest.php b/src/tests/Feature/Controller/Reseller/SkusTest.php
index 421c9e32..cb8291ca 100644
--- a/src/tests/Feature/Controller/Reseller/SkusTest.php
+++ b/src/tests/Feature/Controller/Reseller/SkusTest.php
@@ -1,173 +1,173 @@
<?php
namespace Tests\Feature\Controller\Reseller;
use App\Sku;
use Tests\TestCase;
class SkusTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useResellerUrl();
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
$this->clearMeetEntitlements();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
$this->clearMeetEntitlements();
parent::tearDown();
}
/**
* Test fetching SKUs list for a domain (GET /domains/<id>/skus)
*/
public function testDomainSkus(): void
{
$reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
$reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$user = $this->getTestUser('john@kolab.org');
$domain = $this->getTestDomain('kolab.org');
// Unauth access not allowed
$response = $this->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(401);
// User access not allowed
$response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(403);
// Admin access not allowed
$response = $this->actingAs($admin)->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(403);
// Reseller from another tenant
$response = $this->actingAs($reseller2)->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(404);
// Reseller access
$response = $this->actingAs($reseller1)->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(1, $json);
// Note: Details are tested where we test API\V4\SkusController
}
/**
* Test fetching SKUs list
*/
public function testIndex(): void
{
$reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
$reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$user = $this->getTestUser('john@kolab.org');
$sku = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
// Unauth access not allowed
$response = $this->get("api/v4/skus");
$response->assertStatus(401);
// User access not allowed
$response = $this->actingAs($user)->get("api/v4/skus");
$response->assertStatus(403);
// Admin access not allowed
$response = $this->actingAs($admin)->get("api/v4/skus");
$response->assertStatus(403);
$response = $this->actingAs($reseller1)->get("api/v4/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(13, $json);
$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']);
+ $this->assertSame('Mailbox', $json[0]['handler']);
// Test with another tenant
$sku = Sku::where('title', 'mailbox')->where('tenant_id', $reseller2->tenant_id)->first();
$response = $this->actingAs($reseller2)->get("api/v4/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(6, $json);
$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']);
+ $this->assertSame('Mailbox', $json[0]['handler']);
}
/**
* Test fetching SKUs list for a user (GET /users/<id>/skus)
*/
public function testUserSkus(): void
{
$reseller1 = $this->getTestUser('reseller@' . \config('app.domain'));
$reseller2 = $this->getTestUser('reseller@sample-tenant.dev-local');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$user = $this->getTestUser('john@kolab.org');
// Unauth access not allowed
$response = $this->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(401);
// User access not allowed
$response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(403);
// Admin access not allowed
$response = $this->actingAs($admin)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(403);
// Reseller from another tenant
$response = $this->actingAs($reseller2)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(404);
// Reseller access
$response = $this->actingAs($reseller1)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(6, $json);
// Note: Details are tested where we test API\V4\SkusController
}
}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
index 277c5ec9..c4258d13 100644
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -1,282 +1,282 @@
<?php
namespace Tests\Feature\Controller;
use App\Entitlement;
use App\Http\Controllers\API\V4\SkusController;
use App\Sku;
use App\Tenant;
use Tests\TestCase;
class SkusTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->clearBetaEntitlements();
$this->clearMeetEntitlements();
Sku::where('title', 'test')->delete();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->clearBetaEntitlements();
$this->clearMeetEntitlements();
Sku::where('title', 'test')->delete();
parent::tearDown();
}
/**
* Test fetching SKUs list for a domain (GET /domains/<id>/skus)
*/
public function testDomainSkus(): void
{
$user = $this->getTestUser('john@kolab.org');
$domain = $this->getTestDomain('kolab.org');
// Unauth access not allowed
$response = $this->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(401);
// Create an sku for another tenant, to make sure it is not included in the result
$nsku = Sku::create([
'title' => 'test',
'name' => 'Test',
'description' => '',
'active' => true,
'cost' => 100,
'handler_class' => 'App\Handlers\Domain',
]);
$tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first();
$nsku->tenant_id = $tenant->id;
$nsku->save();
$response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(1, $json);
$this->assertSkuElement('domain-hosting', $json[0], [
'prio' => 0,
'type' => 'domain',
- 'handler' => 'domainhosting',
+ 'handler' => 'DomainHosting',
'enabled' => false,
'readonly' => false,
]);
}
/**
* Test fetching SKUs list
*/
public function testIndex(): void
{
// Unauth access not allowed
$response = $this->get("api/v4/skus");
$response->assertStatus(401);
$user = $this->getTestUser('john@kolab.org');
$sku = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
// Create an sku for another tenant, to make sure it is not included in the result
$nsku = Sku::create([
'title' => 'test',
'name' => 'Test',
'description' => '',
'active' => true,
'cost' => 100,
'handler_class' => 'App\Handlers\Mailbox',
]);
$tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first();
$nsku->tenant_id = $tenant->id;
$nsku->save();
$response = $this->actingAs($user)->get("api/v4/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(13, $json);
$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']);
+ $this->assertSame('Mailbox', $json[0]['handler']);
}
/**
* Test fetching SKUs list for a user (GET /users/<id>/skus)
*/
public function testUserSkus(): void
{
$user = $this->getTestUser('john@kolab.org');
// Unauth access not allowed
$response = $this->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(401);
// Create an sku for another tenant, to make sure it is not included in the result
$nsku = Sku::create([
'title' => 'test',
'name' => 'Test',
'description' => '',
'active' => true,
'cost' => 100,
- 'handler_class' => 'App\Handlers\Mailbox',
+ 'handler_class' => 'Mailbox',
]);
$tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first();
$nsku->tenant_id = $tenant->id;
$nsku->save();
$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',
+ 'handler' => 'Mailbox',
'enabled' => true,
'readonly' => true,
]);
$this->assertSkuElement('storage', $json[1], [
'prio' => 90,
'type' => 'user',
- 'handler' => 'storage',
+ 'handler' => 'Storage',
'enabled' => true,
'readonly' => true,
'range' => [
'min' => 5,
'max' => 100,
'unit' => 'GB',
]
]);
$this->assertSkuElement('groupware', $json[2], [
'prio' => 80,
'type' => 'user',
- 'handler' => 'groupware',
+ 'handler' => 'Groupware',
'enabled' => false,
'readonly' => false,
]);
$this->assertSkuElement('activesync', $json[3], [
'prio' => 70,
'type' => 'user',
- 'handler' => 'activesync',
+ 'handler' => 'Activesync',
'enabled' => false,
'readonly' => false,
- 'required' => ['groupware'],
+ 'required' => ['Groupware'],
]);
$this->assertSkuElement('2fa', $json[4], [
'prio' => 60,
'type' => 'user',
- 'handler' => 'auth2f',
+ 'handler' => 'Auth2F',
'enabled' => false,
'readonly' => false,
- 'forbidden' => ['activesync'],
+ 'forbidden' => ['Activesync'],
]);
$this->assertSkuElement('meet', $json[5], [
'prio' => 50,
'type' => 'user',
- 'handler' => 'meet',
+ 'handler' => 'Meet',
'enabled' => false,
'readonly' => false,
- 'required' => ['groupware'],
+ 'required' => ['Groupware'],
]);
// Test inclusion of beta SKUs
$sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$user->assignSku($sku);
$response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(10, $json);
$this->assertSkuElement('beta', $json[6], [
'prio' => 10,
'type' => 'user',
- 'handler' => 'beta',
+ 'handler' => 'Beta',
'enabled' => false,
'readonly' => false,
]);
- $this->assertSkuElement('beta-resources', $json[7], [
+ $this->assertSkuElement('beta-distlists', $json[7], [
'prio' => 10,
'type' => 'user',
- 'handler' => 'resources', // TODO: shouldn't it be beta-resources or beta/resources?
+ 'handler' => 'Beta\Distlists',
'enabled' => false,
'readonly' => false,
- 'required' => ['beta'],
+ 'required' => ['Beta'],
]);
- $this->assertSkuElement('beta-shared-folders', $json[8], [
+ $this->assertSkuElement('beta-resources', $json[8], [
'prio' => 10,
'type' => 'user',
- 'handler' => 'sharedfolders',
+ 'handler' => 'Beta\Resources',
'enabled' => false,
'readonly' => false,
- 'required' => ['beta'],
+ 'required' => ['Beta'],
]);
- $this->assertSkuElement('distlist', $json[9], [
+ $this->assertSkuElement('beta-shared-folders', $json[9], [
'prio' => 10,
'type' => 'user',
- 'handler' => 'distlist',
+ 'handler' => 'Beta\SharedFolders',
'enabled' => false,
'readonly' => false,
- 'required' => ['beta'],
+ '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::withEnvTenantContext()->where('title', $sku_title)->first();
$this->assertSame($sku->id, $result['id']);
$this->assertSame($sku->title, $result['title']);
$this->assertSame($sku->name, $result['name']);
$this->assertSame($sku->description, $result['description']);
$this->assertSame($sku->cost, $result['cost']);
$this->assertSame($sku->units_free, $result['units_free']);
$this->assertSame($sku->period, $result['period']);
$this->assertSame($sku->active, $result['active']);
foreach ($other as $key => $value) {
$this->assertSame($value, $result[$key]);
}
$this->assertCount(8 + count($other), $result);
}
}
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
index 2f46066a..e1dda81a 100644
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -1,599 +1,599 @@
<?php
namespace Tests;
use App\Backends\LDAP;
use App\Domain;
use App\Group;
use App\Resource;
use App\SharedFolder;
use App\Sku;
use App\Transaction;
use App\User;
use Carbon\Carbon;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Queue;
use PHPUnit\Framework\Assert;
trait TestCaseTrait
{
/**
* A domain that is hosted.
*
* @var ?\App\Domain
*/
protected $domainHosted;
/**
* The hosted domain owner.
*
* @var ?\App\User
*/
protected $domainOwner;
/**
* Some profile details for an owner of a domain
*
* @var array
*/
protected $domainOwnerSettings = [
'first_name' => 'John',
'last_name' => 'Doe',
'organization' => 'Test Domain Owner',
];
/**
* Some users for the hosted domain, ultimately including the owner.
*
* @var \App\User[]
*/
protected $domainUsers = [];
/**
* A specific user that is a regular user in the hosted domain.
*
* @var ?\App\User
*/
protected $jack;
/**
* A specific user that is a controller on the wallet to which the hosted domain is charged.
*
* @var ?\App\User
*/
protected $jane;
/**
* A specific user that has a second factor configured.
*
* @var ?\App\User
*/
protected $joe;
/**
* One of the domains that is available for public registration.
*
* @var ?\App\Domain
*/
protected $publicDomain;
/**
* A newly generated user in a public domain.
*
* @var ?\App\User
*/
protected $publicDomainUser;
/**
* A placeholder for a password that can be generated.
*
* Should be generated with `\App\Utils::generatePassphrase()`.
*
* @var ?string
*/
protected $userPassword;
/**
* Register the beta entitlement for a user
*/
protected function addBetaEntitlement($user, $title): void
{
// Add beta + $title entitlements
$beta_sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$sku = Sku::withEnvTenantContext()->where('title', $title)->first();
$user->assignSku($beta_sku);
$user->assignSku($sku);
}
/**
* Assert that the entitlements for the user match the expected list of entitlements.
*
* @param \App\User|\App\Domain $object The object for which the entitlements need to be pulled.
* @param array $expected An array of expected \App\Sku titles.
*/
protected function assertEntitlements($object, $expected)
{
// Assert the user entitlements
$skus = $object->entitlements()->get()
->map(function ($ent) {
return $ent->sku->title;
})
->toArray();
sort($skus);
Assert::assertSame($expected, $skus);
}
protected function backdateEntitlements($entitlements, $targetDate)
{
$wallets = [];
$ids = [];
foreach ($entitlements as $entitlement) {
$ids[] = $entitlement->id;
$wallets[] = $entitlement->wallet_id;
}
\App\Entitlement::whereIn('id', $ids)->update([
'created_at' => $targetDate,
'updated_at' => $targetDate,
]);
if (!empty($wallets)) {
$wallets = array_unique($wallets);
$owners = \App\Wallet::whereIn('id', $wallets)->pluck('user_id')->all();
\App\User::whereIn('id', $owners)->update(['created_at' => $targetDate]);
}
}
/**
* Removes all beta entitlements from the database
*/
protected function clearBetaEntitlements(): void
{
$beta_handlers = [
'App\Handlers\Beta',
+ 'App\Handlers\Beta\Distlists',
'App\Handlers\Beta\Resources',
'App\Handlers\Beta\SharedFolders',
- 'App\Handlers\Distlist',
];
$betas = Sku::whereIn('handler_class', $beta_handlers)->pluck('id')->all();
\App\Entitlement::whereIn('sku_id', $betas)->delete();
}
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__ . '/../bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
return $app;
}
/**
* Create a set of transaction log entries for a wallet
*/
protected function createTestTransactions($wallet)
{
$result = [];
$date = Carbon::now();
$debit = 0;
$entitlementTransactions = [];
foreach ($wallet->entitlements as $entitlement) {
if ($entitlement->cost) {
$debit += $entitlement->cost;
$entitlementTransactions[] = $entitlement->createTransaction(
Transaction::ENTITLEMENT_BILLED,
$entitlement->cost
);
}
}
$transaction = Transaction::create(
[
'user_email' => 'jeroen@jeroen.jeroen',
'object_id' => $wallet->id,
'object_type' => \App\Wallet::class,
'type' => Transaction::WALLET_DEBIT,
'amount' => $debit * -1,
'description' => 'Payment',
]
);
$result[] = $transaction;
Transaction::whereIn('id', $entitlementTransactions)->update(['transaction_id' => $transaction->id]);
$transaction = Transaction::create(
[
'user_email' => null,
'object_id' => $wallet->id,
'object_type' => \App\Wallet::class,
'type' => Transaction::WALLET_CREDIT,
'amount' => 2000,
'description' => 'Payment',
]
);
$transaction->created_at = $date->next(Carbon::MONDAY);
$transaction->save();
$result[] = $transaction;
$types = [
Transaction::WALLET_AWARD,
Transaction::WALLET_PENALTY,
];
// The page size is 10, so we generate so many to have at least two pages
$loops = 10;
while ($loops-- > 0) {
$type = $types[count($result) % count($types)];
$transaction = Transaction::create([
'user_email' => 'jeroen.@jeroen.jeroen',
'object_id' => $wallet->id,
'object_type' => \App\Wallet::class,
'type' => $type,
'amount' => 11 * (count($result) + 1) * ($type == Transaction::WALLET_PENALTY ? -1 : 1),
'description' => 'TRANS' . $loops,
]);
$transaction->created_at = $date->next(Carbon::MONDAY);
$transaction->save();
$result[] = $transaction;
}
return $result;
}
/**
* Delete a test domain whatever it takes.
*
* @coversNothing
*/
protected function deleteTestDomain($name)
{
Queue::fake();
$domain = Domain::withTrashed()->where('namespace', $name)->first();
if (!$domain) {
return;
}
$job = new \App\Jobs\Domain\DeleteJob($domain->id);
$job->handle();
$domain->forceDelete();
}
/**
* Delete a test group whatever it takes.
*
* @coversNothing
*/
protected function deleteTestGroup($email)
{
Queue::fake();
$group = Group::withTrashed()->where('email', $email)->first();
if (!$group) {
return;
}
LDAP::deleteGroup($group);
$group->forceDelete();
}
/**
* Delete a test resource whatever it takes.
*
* @coversNothing
*/
protected function deleteTestResource($email)
{
Queue::fake();
$resource = Resource::withTrashed()->where('email', $email)->first();
if (!$resource) {
return;
}
LDAP::deleteResource($resource);
$resource->forceDelete();
}
/**
* Delete a test shared folder whatever it takes.
*
* @coversNothing
*/
protected function deleteTestSharedFolder($email)
{
Queue::fake();
$folder = SharedFolder::withTrashed()->where('email', $email)->first();
if (!$folder) {
return;
}
LDAP::deleteSharedFolder($folder);
$folder->forceDelete();
}
/**
* Delete a test user whatever it takes.
*
* @coversNothing
*/
protected function deleteTestUser($email)
{
Queue::fake();
$user = User::withTrashed()->where('email', $email)->first();
if (!$user) {
return;
}
LDAP::deleteUser($user);
$user->forceDelete();
}
/**
* Helper to access protected property of an object
*/
protected static function getObjectProperty($object, $property_name)
{
$reflection = new \ReflectionClass($object);
$property = $reflection->getProperty($property_name);
$property->setAccessible(true);
return $property->getValue($object);
}
/**
* Get Domain object by namespace, create it if needed.
* Skip LDAP jobs.
*
* @coversNothing
*/
protected function getTestDomain($name, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
return Domain::firstOrCreate(['namespace' => $name], $attrib);
}
/**
* Get Group object by email, create it if needed.
* Skip LDAP jobs.
*/
protected function getTestGroup($email, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
return Group::firstOrCreate(['email' => $email], $attrib);
}
/**
* Get Resource object by email, create it if needed.
* Skip LDAP jobs.
*/
protected function getTestResource($email, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
$resource = Resource::where('email', $email)->first();
if (!$resource) {
list($local, $domain) = explode('@', $email, 2);
$resource = new Resource();
$resource->email = $email;
$resource->domain = $domain;
if (!isset($attrib['name'])) {
$resource->name = $local;
}
}
foreach ($attrib as $key => $val) {
$resource->{$key} = $val;
}
$resource->save();
return $resource;
}
/**
* Get SharedFolder object by email, create it if needed.
* Skip LDAP jobs.
*/
protected function getTestSharedFolder($email, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
$folder = SharedFolder::where('email', $email)->first();
if (!$folder) {
list($local, $domain) = explode('@', $email, 2);
$folder = new SharedFolder();
$folder->email = $email;
$folder->domain = $domain;
if (!isset($attrib['name'])) {
$folder->name = $local;
}
}
foreach ($attrib as $key => $val) {
$folder->{$key} = $val;
}
$folder->save();
return $folder;
}
/**
* Get User object by email, create it if needed.
* Skip LDAP jobs.
*
* @coversNothing
*/
protected function getTestUser($email, $attrib = [])
{
// Disable jobs (i.e. skip LDAP oprations)
Queue::fake();
$user = User::firstOrCreate(['email' => $email], $attrib);
if ($user->trashed()) {
// Note: we do not want to use user restore here
User::where('id', $user->id)->forceDelete();
$user = User::create(['email' => $email] + $attrib);
}
return $user;
}
/**
* Call protected/private method of a class.
*
* @param object $object Instantiated object that we will run method on.
* @param string $methodName Method name to call
* @param array $parameters Array of parameters to pass into method.
*
* @return mixed Method return.
*/
protected function invokeMethod($object, $methodName, array $parameters = array())
{
$reflection = new \ReflectionClass(get_class($object));
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);
return $method->invokeArgs($object, $parameters);
}
protected function setUpTest()
{
$this->userPassword = \App\Utils::generatePassphrase();
$this->domainHosted = $this->getTestDomain(
'test.domain',
[
'type' => \App\Domain::TYPE_EXTERNAL,
'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED
]
);
$this->getTestDomain(
'test2.domain2',
[
'type' => \App\Domain::TYPE_EXTERNAL,
'status' => \App\Domain::STATUS_ACTIVE | \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_VERIFIED
]
);
$packageKolab = \App\Package::where('title', 'kolab')->first();
$this->domainOwner = $this->getTestUser('john@test.domain', ['password' => $this->userPassword]);
$this->domainOwner->assignPackage($packageKolab);
$this->domainOwner->setSettings($this->domainOwnerSettings);
$this->domainOwner->setAliases(['alias1@test2.domain2']);
// separate for regular user
$this->jack = $this->getTestUser('jack@test.domain', ['password' => $this->userPassword]);
// separate for wallet controller
$this->jane = $this->getTestUser('jane@test.domain', ['password' => $this->userPassword]);
$this->joe = $this->getTestUser('joe@test.domain', ['password' => $this->userPassword]);
$this->domainUsers[] = $this->jack;
$this->domainUsers[] = $this->jane;
$this->domainUsers[] = $this->joe;
$this->domainUsers[] = $this->getTestUser('jill@test.domain', ['password' => $this->userPassword]);
foreach ($this->domainUsers as $user) {
$this->domainOwner->assignPackage($packageKolab, $user);
}
$this->domainUsers[] = $this->domainOwner;
// assign second factor to joe
$this->joe->assignSku(Sku::where('title', '2fa')->first());
\App\Auth\SecondFactor::seed($this->joe->email);
usort(
$this->domainUsers,
function ($a, $b) {
return $a->email > $b->email;
}
);
$this->domainHosted->assignPackage(
\App\Package::where('title', 'domain-hosting')->first(),
$this->domainOwner
);
$wallet = $this->domainOwner->wallets()->first();
$wallet->addController($this->jane);
$this->publicDomain = \App\Domain::where('type', \App\Domain::TYPE_PUBLIC)->first();
$this->publicDomainUser = $this->getTestUser(
'john@' . $this->publicDomain->namespace,
['password' => $this->userPassword]
);
$this->publicDomainUser->assignPackage($packageKolab);
}
public function tearDown(): void
{
foreach ($this->domainUsers as $user) {
if ($user == $this->domainOwner) {
continue;
}
$this->deleteTestUser($user->email);
}
if ($this->domainOwner) {
$this->deleteTestUser($this->domainOwner->email);
}
if ($this->domainHosted) {
$this->deleteTestDomain($this->domainHosted->namespace);
}
if ($this->publicDomainUser) {
$this->deleteTestUser($this->publicDomainUser->email);
}
parent::tearDown();
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Feb 1, 4:24 PM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426745
Default Alt Text
(205 KB)

Event Timeline