Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2528620
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
205 KB
Referenced Files
None
Subscribers
None
View Options
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">
¹ {{ $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">
¹ {{ $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
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Feb 1, 4:24 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426745
Default Alt Text
(205 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment