diff --git a/docker/redis/Dockerfile b/docker/redis/Dockerfile --- a/docker/redis/Dockerfile +++ b/docker/redis/Dockerfile @@ -1,4 +1,4 @@ -FROM fedora:30 +FROM fedora:34 ENV container docker ENV SYSTEMD_PAGER='' diff --git a/src/.env.example b/src/.env.example --- a/src/.env.example +++ b/src/.env.example @@ -3,10 +3,11 @@ APP_KEY= APP_DEBUG=true APP_URL=http://127.0.0.1:8000 +#APP_PASSPHRASE= APP_PUBLIC_URL= APP_DOMAIN=kolabnow.com APP_THEME=default -APP_TENANT_ID=1 +APP_TENANT_ID=5 APP_LOCALE=en APP_LOCALES=en,de @@ -34,6 +35,8 @@ SESSION_DRIVER=file SESSION_LIFETIME=120 +OPENEXCHANGERATES_API_KEY="from openexchangerates.org" + MFA_DSN=mysql://roundcube:Welcome2KolabSystems@127.0.0.1/roundcube MFA_TOTP_DIGITS=6 MFA_TOTP_INTERVAL=30 diff --git a/src/app/Backends/OpenExchangeRates.php b/src/app/Backends/OpenExchangeRates.php --- a/src/app/Backends/OpenExchangeRates.php +++ b/src/app/Backends/OpenExchangeRates.php @@ -7,7 +7,7 @@ /** * Import exchange rates from openexchangerates.org * - * @param string Base currency + * @param string $baseCurrency Base currency * * @return array exchange rates */ diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php --- a/src/app/Console/Command.php +++ b/src/app/Console/Command.php @@ -77,6 +77,10 @@ $model = new $objectClass(); } + if ($this->commandPrefix == 'scalpel') { + return $model; + } + $modelsWithTenant = [ \App\Discount::class, \App\Domain::class, @@ -91,22 +95,20 @@ \App\Wallet::class, ]; - $tenant_id = \config('app.tenant_id'); + $tenantId = \config('app.tenant_id'); // Add tenant filter if (in_array($objectClass, $modelsWithTenant)) { - $model = $model->withEnvTenant(); + $model = $model->withEnvTenantContext(); } elseif (in_array($objectClass, $modelsWithOwner)) { - $model = $model->whereExists(function ($query) use ($tenant_id) { + $model = $model->whereExists(function ($query) use ($tenantId) { $query->select(DB::raw(1)) ->from('users') ->whereRaw('wallets.user_id = users.id') - ->whereRaw('users.tenant_id ' . ($tenant_id ? "= $tenant_id" : 'is null')); + ->whereRaw('users.tenant_id ' . ($tenantId ? "= $tenantId" : 'is null')); }); } - // TODO: tenant check for Entitlement, Transaction, etc. - return $model; } diff --git a/src/app/Console/Commands/Domain/CreateCommand.php b/src/app/Console/Commands/Domain/CreateCommand.php --- a/src/app/Console/Commands/Domain/CreateCommand.php +++ b/src/app/Console/Commands/Domain/CreateCommand.php @@ -19,7 +19,7 @@ * * @var string */ - protected $description = "Create a domain."; + protected $description = "Create a domain"; /** * Execute the console command. diff --git a/src/app/Console/Commands/Domain/DeleteCommand.php b/src/app/Console/Commands/Domain/DeleteCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Domain/DeleteCommand.php @@ -0,0 +1,34 @@ +argument('domain'); + + $domain = $this->getDomain($argument); + + if (!$domain) { + $this->error("No such domain {$argument}"); + return 1; + } + + if ($domain->isPublic()) { + $this->error("This domain is a public registration domain."); + return 1; + } + + parent::handle(); + } +} diff --git a/src/app/Console/Commands/Domain/UsersCommand.php b/src/app/Console/Commands/Domain/UsersCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Domain/UsersCommand.php @@ -0,0 +1,13 @@ +getDomain($this->argument('domain')); - - if (!$domain) { - return 1; - } - - $domain->delete(); - } -} diff --git a/src/app/Console/Commands/DomainList.php b/src/app/Console/Commands/DomainList.php --- a/src/app/Console/Commands/DomainList.php +++ b/src/app/Console/Commands/DomainList.php @@ -34,7 +34,7 @@ $domains = Domain::orderBy('namespace'); } - $domains->withEnvTenant()->each( + $domains->withEnvTenantContext()->each( function ($domain) { $msg = $domain->namespace; diff --git a/src/app/Console/Commands/DomainListUsers.php b/src/app/Console/Commands/DomainListUsers.php deleted file mode 100644 --- a/src/app/Console/Commands/DomainListUsers.php +++ /dev/null @@ -1,83 +0,0 @@ -getDomain($this->argument('domain')); - - if (!$domain) { - return 1; - } - - if ($domain->isPublic()) { - $this->error("This domain is a public registration domain."); - return 1; - } - - // TODO: actually implement listing users - $wallet = $domain->wallet(); - - if (!$wallet) { - $this->error("This domain isn't billed to a wallet."); - return 1; - } - - $mailboxSKU = \App\Sku::where('title', 'mailbox')->first(); - - if (!$mailboxSKU) { - $this->error("No mailbox SKU available."); - } - - $entitlements = $wallet->entitlements() - ->where('entitleable_type', \App\User::class) - ->where('sku_id', $mailboxSKU->id)->get(); - - $users = []; - - foreach ($entitlements as $entitlement) { - $users[] = $entitlement->entitleable; - } - - usort($users, function ($a, $b) { - return $a->email > $b->email; - }); - - foreach ($users as $user) { - $this->info($user->email); - } - } -} diff --git a/src/app/Console/Commands/PackageSkus.php b/src/app/Console/Commands/PackageSkus.php --- a/src/app/Console/Commands/PackageSkus.php +++ b/src/app/Console/Commands/PackageSkus.php @@ -28,7 +28,7 @@ */ public function handle() { - $packages = Package::withEnvTenant()->get(); + $packages = Package::withEnvTenantContext()->get(); foreach ($packages as $package) { $this->info(sprintf("Package: %s", $package->title)); diff --git a/src/app/Console/Commands/PlanPackages.php b/src/app/Console/Commands/PlanPackages.php --- a/src/app/Console/Commands/PlanPackages.php +++ b/src/app/Console/Commands/PlanPackages.php @@ -38,7 +38,7 @@ */ public function handle() { - $plans = Plan::withEnvTenant()->get(); + $plans = Plan::withEnvTenantContext()->get(); foreach ($plans as $plan) { $this->info(sprintf("Plan: %s", $plan->title)); diff --git a/src/app/Console/Commands/User/DeleteCommand.php b/src/app/Console/Commands/User/DeleteCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/User/DeleteCommand.php @@ -0,0 +1,15 @@ +withEnvTenantContext()->where('email', $this->argument('user'))->first(); + + if (!$user) { + $user = \App\User::withTrashed()->withEnvTenantContext()->where('id', $this->argument('user'))->first(); + } + + if (!$user) { + $this->error("No such user '" . $this->argument('user') . "' within this tenant context."); + $this->info("Try ./artisan scalpel:user:read --attr=email --attr=tenant_id " . $this->argument('user')); + return 1; + } + + $statuses = [ + 'active' => \App\User::STATUS_ACTIVE, + 'suspended' => \App\User::STATUS_SUSPENDED, + 'deleted' => \App\User::STATUS_DELETED, + 'ldapReady' => \App\User::STATUS_LDAP_READY, + 'imapReady' => \App\User::STATUS_IMAP_READY, + ]; + + $user_state = []; + + foreach (\array_keys($statuses) as $state) { + $func = 'is' . \ucfirst($state); + if ($user->$func()) { + $user_state[] = $state; + } + } + + $this->info("Status: " . \implode(',', $user_state)); + } +} diff --git a/src/app/Console/Commands/UserStatus.php b/src/app/Console/Commands/UserStatus.php deleted file mode 100644 --- a/src/app/Console/Commands/UserStatus.php +++ /dev/null @@ -1,53 +0,0 @@ -getUser($this->argument('user')); - - if (!$user) { - return 1; - } - - $statuses = [ - 'active' => User::STATUS_ACTIVE, - 'suspended' => User::STATUS_SUSPENDED, - 'deleted' => User::STATUS_DELETED, - 'ldapReady' => User::STATUS_LDAP_READY, - 'imapReady' => User::STATUS_IMAP_READY, - ]; - - foreach ($statuses as $text => $bit) { - $func = 'is' . \ucfirst($text); - - $this->info(sprintf("%d %s: %s", $bit, $text, $user->$func())); - } - - $this->info("In total: {$user->status}"); - } -} diff --git a/src/app/Console/Commands/WalletBalances.php b/src/app/Console/Commands/WalletBalances.php --- a/src/app/Console/Commands/WalletBalances.php +++ b/src/app/Console/Commands/WalletBalances.php @@ -29,7 +29,7 @@ { $wallets = \App\Wallet::select('wallets.*') ->join('users', 'users.id', '=', 'wallets.user_id') - ->withEnvTenant('users') + ->withEnvTenantContext('users') ->all(); $wallets->each( diff --git a/src/app/Console/Commands/WalletCharge.php b/src/app/Console/Commands/WalletCharge.php --- a/src/app/Console/Commands/WalletCharge.php +++ b/src/app/Console/Commands/WalletCharge.php @@ -41,7 +41,7 @@ // Get all wallets, excluding deleted accounts $wallets = Wallet::select('wallets.*') ->join('users', 'users.id', '=', 'wallets.user_id') - ->withEnvTenant('users') + ->withEnvTenantContext('users') ->whereNull('users.deleted_at') ->cursor(); } diff --git a/src/app/Console/Commands/WalletExpected.php b/src/app/Console/Commands/WalletExpected.php --- a/src/app/Console/Commands/WalletExpected.php +++ b/src/app/Console/Commands/WalletExpected.php @@ -38,7 +38,7 @@ } else { $wallets = \App\Wallet::select('wallets.*') ->join('users', 'users.id', '=', 'wallets.user_id') - ->withEnvTenant('users') + ->withEnvTenantContext('users') ->all(); } diff --git a/src/app/Console/Development/UserStatus.php b/src/app/Console/Development/UserStatus.php deleted file mode 100644 --- a/src/app/Console/Development/UserStatus.php +++ /dev/null @@ -1,86 +0,0 @@ -argument('userid'))->firstOrFail(); - - $this->info("Found user: {$user->id}"); - - $statuses = [ - 'active' => User::STATUS_ACTIVE, - 'suspended' => User::STATUS_SUSPENDED, - 'deleted' => User::STATUS_DELETED, - 'ldapReady' => User::STATUS_LDAP_READY, - 'imapReady' => User::STATUS_IMAP_READY, - ]; - - // I'd prefer "-state" and "+state" syntax, but it's not possible - $delete = false; - if ($update = $this->option('del')) { - $delete = true; - } elseif ($update = $this->option('add')) { - // do nothing - } - - if (!empty($update)) { - $map = \array_change_key_case($statuses); - $update = \strtolower($update); - - if (isset($map[$update])) { - if ($delete && $user->status & $map[$update]) { - $user->status ^= $map[$update]; - $user->save(); - } elseif (!$delete && !($user->status & $map[$update])) { - $user->status |= $map[$update]; - $user->save(); - } - } - } - - $user_state = []; - foreach (\array_keys($statuses) as $state) { - $func = 'is' . \ucfirst($state); - if ($user->$func()) { - $user_state[] = $state; - } - } - - $this->info("Status: " . \implode(',', $user_state)); - } -} diff --git a/src/app/Console/ObjectDeleteCommand.php b/src/app/Console/ObjectDeleteCommand.php --- a/src/app/Console/ObjectDeleteCommand.php +++ b/src/app/Console/ObjectDeleteCommand.php @@ -36,6 +36,10 @@ $classes = class_uses_recursive($this->objectClass); + if (in_array(SoftDeletes::class, $classes)) { + $this->signature .= " {--with-deleted : Consider deleted {$this->objectName}s}"; + } + parent::__construct(); } @@ -87,9 +91,19 @@ if ($this->commandPrefix == 'scalpel') { $this->objectClass::withoutEvents( function () use ($object) { - $object->delete(); + if ($object->deleted_at) { + $object->forceDelete(); + } else { + $object->delete(); + } } ); + } else { + if ($object->deleted_at) { + $object->forceDelete(); + } else { + $object->delete(); + } } } } diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -115,7 +115,7 @@ */ public static function getPublicDomains(): array { - return self::withEnvTenant() + return self::withEnvTenantContext() ->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC)) ->get(['namespace'])->pluck('namespace')->toArray(); } @@ -419,6 +419,43 @@ $this->save(); } + /** + * List the users of a domain, so long as the domain is not a public registration domain. + * + * @return array + */ + public function users(): array + { + if ($this->isPublic()) { + return []; + } + + $wallet = $this->wallet(); + + if (!$wallet) { + return []; + } + + $mailboxSKU = \App\Sku::withObjectTenantContext($this)->where('title', 'mailbox')->first(); + + if (!$mailboxSKU) { + \Log::error("No mailbox SKU available."); + return []; + } + + $entitlements = $wallet->entitlements() + ->where('entitleable_type', \App\User::class) + ->where('sku_id', $mailboxSKU->id)->get(); + + $users = []; + + foreach ($entitlements as $entitlement) { + $users[] = $entitlement->entitleable; + } + + return $users; + } + /** * Verify if a domain exists in DNS * diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -121,7 +121,7 @@ */ public function invitation($id) { - $invitation = SignupInvitation::withEnvTenant()->find($id); + $invitation = SignupInvitation::withEnvTenantContext()->find($id); if (empty($invitation) || $invitation->isCompleted()) { return $this->errorResponse(404); @@ -218,7 +218,7 @@ // Signup via invitation if ($request->invitation) { - $invitation = SignupInvitation::withEnvTenant()->find($request->invitation); + $invitation = SignupInvitation::withEnvTenantContext()->find($request->invitation); if (empty($invitation) || $invitation->isCompleted()) { return $this->errorResponse(404); diff --git a/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php b/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php --- a/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/DiscountsController.php @@ -16,7 +16,7 @@ { $discounts = []; - Discount::withEnvTenant() + Discount::withEnvTenantContext() ->where('active', true) ->orderBy('discount') ->get() diff --git a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php --- a/src/app/Http/Controllers/API/V4/Admin/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/DomainsController.php @@ -20,7 +20,7 @@ $result = collect([]); if ($owner) { - if ($owner = User::withEnvTenant()->find($owner)) { + if ($owner = User::find($owner)) { foreach ($owner->wallets as $wallet) { $entitlements = $wallet->entitlements()->where('entitleable_type', Domain::class)->get(); @@ -33,7 +33,7 @@ $result = $result->sortBy('namespace')->values(); } } elseif (!empty($search)) { - if ($domain = Domain::withEnvTenant()->where('namespace', $search)->first()) { + if ($domain = Domain::where('namespace', $search)->first()) { $result->push($domain); } } @@ -64,7 +64,7 @@ */ public function suspend(Request $request, $id) { - $domain = Domain::withEnvTenant()->find($id); + $domain = Domain::find($id); if (empty($domain) || $domain->isPublic()) { return $this->errorResponse(404); @@ -88,7 +88,7 @@ */ public function unsuspend(Request $request, $id) { - $domain = Domain::withEnvTenant()->find($id); + $domain = Domain::find($id); if (empty($domain) || $domain->isPublic()) { return $this->errorResponse(404); diff --git a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php --- a/src/app/Http/Controllers/API/V4/Admin/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/GroupsController.php @@ -20,7 +20,7 @@ $result = collect([]); if ($owner) { - if ($owner = User::withEnvTenant()->find($owner)) { + if ($owner = User::find($owner)) { foreach ($owner->wallets as $wallet) { $wallet->entitlements()->where('entitleable_type', Group::class)->get() ->each(function ($entitlement) use ($result) { @@ -31,7 +31,7 @@ $result = $result->sortBy('namespace')->values(); } } elseif (!empty($search)) { - if ($group = Group::withEnvTenant()->where('email', $search)->first()) { + if ($group = Group::where('email', $search)->first()) { $result->push($group); } } @@ -78,7 +78,7 @@ */ public function suspend(Request $request, $id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::find($id); if (empty($group)) { return $this->errorResponse(404); @@ -102,7 +102,7 @@ */ public function unsuspend(Request $request, $id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::find($id); if (empty($group)) { return $this->errorResponse(404); diff --git a/src/app/Http/Controllers/API/V4/Admin/SkusController.php b/src/app/Http/Controllers/API/V4/Admin/SkusController.php --- a/src/app/Http/Controllers/API/V4/Admin/SkusController.php +++ b/src/app/Http/Controllers/API/V4/Admin/SkusController.php @@ -2,6 +2,58 @@ namespace App\Http\Controllers\API\V4\Admin; +use App\Sku; +use Illuminate\Support\Facades\Auth; + class SkusController extends \App\Http\Controllers\API\V4\SkusController { + /** + * Get a list of SKUs available to the user. + * + * @param int $id User identifier + * + * @return \Illuminate\Http\JsonResponse + */ + public function userSkus($id) + { + $user = \App\User::find($id); + + if (empty($user)) { + return $this->errorResponse(404); + } + + if (!Auth::guard()->user()->canRead($user)) { + return $this->errorResponse(403); + } + + $type = request()->input('type'); + $response = []; + + // Note: Order by title for consistent ordering in tests + $skus = Sku::withObjectTenantContext($user)->orderBy('title')->get(); + + foreach ($skus as $sku) { + if (!class_exists($sku->handler_class)) { + continue; + } + + if (!$sku->handler_class::isAvailable($sku, $user)) { + continue; + } + + if ($data = $this->skuElement($sku)) { + if ($type && $type != $data['type']) { + continue; + } + + $response[] = $data; + } + } + + usort($response, function ($a, $b) { + return ($b['prio'] <=> $a['prio']); + }); + + return response()->json($response); + } } diff --git a/src/app/Http/Controllers/API/V4/Admin/StatsController.php b/src/app/Http/Controllers/API/V4/Admin/StatsController.php --- a/src/app/Http/Controllers/API/V4/Admin/StatsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/StatsController.php @@ -358,12 +358,10 @@ if ($addQuery) { $query = $addQuery($query, \config('app.tenant_id')); } else { - $query = $query->withEnvTenant(); + $query = $query->withEnvTenantContext(); } } - // TODO: Tenant selector for admins - return $query; } } diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php --- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php @@ -8,6 +8,7 @@ use App\User; use App\UserAlias; use App\UserSetting; +use App\Wallet; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; @@ -37,19 +38,14 @@ $result = collect([]); if ($owner) { - $owner = User::where('id', $owner) - ->withEnvTenant() - ->whereNull('role') - ->first(); + $owner = User::find($owner); if ($owner) { - $result = $owner->users(false)->whereNull('role')->orderBy('email')->get(); + $result = $owner->users(false)->orderBy('email')->get(); } } elseif (strpos($search, '@')) { // Search by email $result = User::withTrashed()->where('email', $search) - ->withEnvTenant() - ->whereNull('role') ->orderBy('email') ->get(); @@ -72,8 +68,6 @@ if (!$user_ids->isEmpty()) { $result = User::withTrashed()->whereIn('id', $user_ids) - ->withEnvTenant() - ->whereNull('role') ->orderBy('email') ->get(); } @@ -81,36 +75,39 @@ } elseif (is_numeric($search)) { // Search by user ID $user = User::withTrashed()->where('id', $search) - ->withEnvTenant() - ->whereNull('role') ->first(); if ($user) { $result->push($user); } - } elseif (!empty($search)) { + } elseif (strpos($search, '.') !== false) { // Search by domain $domain = Domain::withTrashed()->where('namespace', $search) - ->withEnvTenant() ->first(); if ($domain) { - if ( - ($wallet = $domain->wallet()) - && ($owner = $wallet->owner()->withTrashed()->withEnvTenant()->first()) - && empty($owner->role) - ) { + if (($wallet = $domain->wallet()) && ($owner = $wallet->owner()->withTrashed()->first())) { + $result->push($owner); + } + } + } elseif (!empty($search)) { + $wallet = Wallet::find($search); + + if ($wallet) { + if ($owner = $wallet->owner()->withTrashed()->first()) { $result->push($owner); } } } // Process the result - $result = $result->map(function ($user) { - $data = $user->toArray(); - $data = array_merge($data, self::userStatuses($user)); - return $data; - }); + $result = $result->map( + function ($user) { + $data = $user->toArray(); + $data = array_merge($data, self::userStatuses($user)); + return $data; + } + ); $result = [ 'list' => $result, @@ -131,7 +128,7 @@ */ public function reset2FA(Request $request, $id) { - $user = User::withEnvTenant()->find($id); + $user = User::find($id); if (empty($user) || !$this->guard()->user()->canUpdate($user)) { return $this->errorResponse(404); @@ -151,6 +148,42 @@ ]); } + /** + * Display information on the user account specified by $id. + * + * @param int $id The account to show information for. + * + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + $user = User::find($id); + + if (empty($user)) { + return $this->errorResponse(404); + } + + if (!$this->guard()->user()->canRead($user)) { + return $this->errorResponse(403); + } + + $response = $this->userResponse($user); + + // Simplified Entitlement/SKU information, + // TODO: I agree this format may need to be extended in future + $response['skus'] = []; + foreach ($user->entitlements as $ent) { + $sku = $ent->sku; + if (!isset($response['skus'][$sku->id])) { + $response['skus'][$sku->id] = ['costs' => [], 'count' => 0]; + } + $response['skus'][$sku->id]['count']++; + $response['skus'][$sku->id]['costs'][] = $ent->cost; + } + + return response()->json($response); + } + /** * Create a new user record. * @@ -173,7 +206,7 @@ */ public function suspend(Request $request, $id) { - $user = User::withEnvTenant()->find($id); + $user = User::find($id); if (empty($user) || !$this->guard()->user()->canUpdate($user)) { return $this->errorResponse(404); @@ -197,7 +230,7 @@ */ public function unsuspend(Request $request, $id) { - $user = User::withEnvTenant()->find($id); + $user = User::find($id); if (empty($user) || !$this->guard()->user()->canUpdate($user)) { return $this->errorResponse(404); @@ -221,7 +254,7 @@ */ public function update(Request $request, $id) { - $user = User::withEnvTenant()->find($id); + $user = User::find($id); if (empty($user) || !$this->guard()->user()->canUpdate($user)) { return $this->errorResponse(404); diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php --- a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php @@ -138,7 +138,7 @@ if (empty($request->discount)) { $wallet->discount()->dissociate(); $wallet->save(); - } elseif ($discount = Discount::withEnvTenant()->find($request->discount)) { + } elseif ($discount = Discount::withEnvTenantContext()->find($request->discount)) { $wallet->discount()->associate($discount); $wallet->save(); } diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php --- a/src/app/Http/Controllers/API/V4/DomainsController.php +++ b/src/app/Http/Controllers/API/V4/DomainsController.php @@ -117,7 +117,7 @@ */ public function show($id) { - $domain = Domain::withEnvTenant()->findOrFail($id); + $domain = Domain::withEnvTenantContext()->findOrFail($id); // Only owner (or admin) has access to the domain if (!Auth::guard()->user()->canRead($domain)) { @@ -152,7 +152,7 @@ */ public function status($id) { - $domain = Domain::withEnvTenant()->findOrFail($id); + $domain = Domain::withEnvTenantContext()->findOrFail($id); // Only owner (or admin) has access to the domain if (!Auth::guard()->user()->canRead($domain)) { diff --git a/src/app/Http/Controllers/API/V4/GroupsController.php b/src/app/Http/Controllers/API/V4/GroupsController.php --- a/src/app/Http/Controllers/API/V4/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/GroupsController.php @@ -32,7 +32,7 @@ */ public function destroy($id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::withEnvTenantContext()->find($id); if (empty($group)) { return $this->errorResponse(404); @@ -96,7 +96,7 @@ */ public function show($id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::withEnvTenantContext()->find($id); if (empty($group)) { return $this->errorResponse(404); @@ -123,7 +123,7 @@ */ public function status($id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::withEnvTenantContext()->find($id); if (empty($group)) { return $this->errorResponse(404); @@ -308,7 +308,7 @@ */ public function update(Request $request, $id) { - $group = Group::withEnvTenant()->find($id); + $group = Group::withEnvTenantContext()->find($id); if (empty($group)) { return $this->errorResponse(404); diff --git a/src/app/Http/Controllers/API/V4/PackagesController.php b/src/app/Http/Controllers/API/V4/PackagesController.php --- a/src/app/Http/Controllers/API/V4/PackagesController.php +++ b/src/app/Http/Controllers/API/V4/PackagesController.php @@ -54,7 +54,7 @@ { // TODO: Packages should have an 'active' flag too, I guess $response = []; - $packages = Package::select()->orderBy('title')->get(); + $packages = Package::withEnvTenantContext()->select()->orderBy('title')->get(); foreach ($packages as $package) { $response[] = [ diff --git a/src/app/Http/Controllers/API/V4/Reseller/InvitationsController.php b/src/app/Http/Controllers/API/V4/Reseller/InvitationsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/InvitationsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/InvitationsController.php @@ -28,7 +28,7 @@ */ public function destroy($id) { - $invitation = SignupInvitation::withUserTenant()->find($id); + $invitation = SignupInvitation::withSubjectTenantContext()->find($id); if (empty($invitation)) { return $this->errorResponse(404); @@ -66,7 +66,7 @@ $page = intval(request()->input('page')) ?: 1; $hasMore = false; - $result = SignupInvitation::withUserTenant() + $result = SignupInvitation::withSubjectTenantContext() ->latest() ->limit($pageSize + 1) ->offset($pageSize * ($page - 1)); @@ -108,7 +108,7 @@ */ public function resend($id) { - $invitation = SignupInvitation::withUserTenant()->find($id); + $invitation = SignupInvitation::withSubjectTenantContext()->find($id); if (empty($invitation)) { return $this->errorResponse(404); diff --git a/src/app/Http/Controllers/API/V4/Reseller/StatsController.php b/src/app/Http/Controllers/API/V4/Reseller/StatsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/StatsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/StatsController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers\API\V4\Reseller; +use Illuminate\Support\Facades\Auth; + class StatsController extends \App\Http\Controllers\API\V4\Admin\StatsController { /** @var array List of enabled charts */ @@ -11,4 +13,25 @@ 'users', 'users-all', ]; + + /** + * Add tenant scope to the queries when needed + * + * @param \Illuminate\Database\Query\Builder $query The query + * @param callable $addQuery Additional tenant-scope query-modifier + * + * @return \Illuminate\Database\Query\Builder + */ + protected function applyTenantScope($query, $addQuery = null) + { + $user = Auth::guard()->user(); + + if ($addQuery) { + $query = $addQuery($query, $user->tenant_id); + } else { + $query = $query->withSubjectTenantContext(); + } + + return $query; + } } diff --git a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php --- a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php @@ -2,6 +2,256 @@ namespace App\Http\Controllers\API\V4\Reseller; +use App\Domain; +use App\Group; +use App\Sku; +use App\User; +use App\UserAlias; +use App\UserSetting; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Validator; + class UsersController extends \App\Http\Controllers\API\V4\Admin\UsersController { + /** + * Delete a user. + * + * @param int $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function destroy($id) + { + return $this->errorResponse(404); + } + + /** + * Searching of user accounts. + * + * @return \Illuminate\Http\JsonResponse + */ + public function index() + { + $search = trim(request()->input('search')); + $owner = trim(request()->input('owner')); + $result = collect([]); + + if ($owner) { + $owner = User::where('id', $owner) + ->withSubjectTenantContext() + ->whereNull('role') + ->first(); + + if ($owner) { + $result = $owner->users(false)->whereNull('role')->orderBy('email')->get(); + } + } elseif (strpos($search, '@')) { + // Search by email + $result = User::withTrashed()->where('email', $search) + ->withSubjectTenantContext() + ->whereNull('role') + ->orderBy('email') + ->get(); + + if ($result->isEmpty()) { + // Search by an alias + $user_ids = UserAlias::where('alias', $search)->get()->pluck('user_id'); + + // Search by an external email + $ext_user_ids = UserSetting::where('key', 'external_email') + ->where('value', $search) + ->get() + ->pluck('user_id'); + + $user_ids = $user_ids->merge($ext_user_ids)->unique(); + + // Search by a distribution list email + if ($group = Group::withTrashed()->where('email', $search)->first()) { + $user_ids = $user_ids->merge([$group->wallet()->user_id])->unique(); + } + + if (!$user_ids->isEmpty()) { + $result = User::withTrashed()->whereIn('id', $user_ids) + ->withSubjectTenantContext() + ->whereNull('role') + ->orderBy('email') + ->get(); + } + } + } elseif (is_numeric($search)) { + // Search by user ID + $user = User::withTrashed()->where('id', $search) + ->withSubjectTenantContext() + ->whereNull('role') + ->first(); + + if ($user) { + $result->push($user); + } + } elseif (!empty($search)) { + // Search by domain + $domain = Domain::withTrashed()->where('namespace', $search) + ->withSubjectTenantContext() + ->first(); + + if ($domain) { + if ( + ($wallet = $domain->wallet()) + && ($owner = $wallet->owner()->withTrashed()->withSubjectTenantContext()->first()) + && empty($owner->role) + ) { + $result->push($owner); + } + } + } + + // Process the result + $result = $result->map(function ($user) { + $data = $user->toArray(); + $data = array_merge($data, self::userStatuses($user)); + return $data; + }); + + $result = [ + 'list' => $result, + 'count' => count($result), + 'message' => \trans('app.search-foundxusers', ['x' => count($result)]), + ]; + + return response()->json($result); + } + + /** + * Reset 2-Factor Authentication for the user + * + * @param \Illuminate\Http\Request $request The API request. + * @param string $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function reset2FA(Request $request, $id) + { + $user = User::withSubjectTenantContext()->find($id); + + if (empty($user) || !$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(404); + } + + $sku = Sku::where('title', '2fa')->first(); + + // Note: we do select first, so the observer can delete + // 2FA preferences from Roundcube database, so don't + // be tempted to replace first() with delete() below + $entitlement = $user->entitlements()->where('sku_id', $sku->id)->first(); + $entitlement->delete(); + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-reset-2fa-success'), + ]); + } + + /** + * Create a new user record. + * + * @param \Illuminate\Http\Request $request The API request. + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function store(Request $request) + { + return $this->errorResponse(404); + } + + /** + * Suspend the user + * + * @param \Illuminate\Http\Request $request The API request. + * @param string $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function suspend(Request $request, $id) + { + $user = User::withSubjectTenantContext()->find($id); + + if (empty($user) || !$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(404); + } + + $user->suspend(); + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-suspend-success'), + ]); + } + + /** + * Un-Suspend the user + * + * @param \Illuminate\Http\Request $request The API request. + * @param string $id User identifier + * + * @return \Illuminate\Http\JsonResponse The response + */ + public function unsuspend(Request $request, $id) + { + $user = User::withSubjectTenantContext()->find($id); + + if (empty($user) || !$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(404); + } + + $user->unsuspend(); + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-unsuspend-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::withSubjectTenantContext()->find($id); + + if (empty($user) || !$this->guard()->user()->canUpdate($user)) { + return $this->errorResponse(404); + } + + // For now admins can change only user external email address + + $rules = []; + + if (array_key_exists('external_email', $request->input())) { + $rules['external_email'] = 'email'; + } + + // Validate input + $v = Validator::make($request->all(), $rules); + + if ($v->fails()) { + return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); + } + + // Update user settings + $settings = $request->only(array_keys($rules)); + + if (!empty($settings)) { + $user->setSettings($settings); + } + + return response()->json([ + 'status' => 'success', + 'message' => __('app.user-update-success'), + ]); + } } diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php --- a/src/app/Http/Controllers/API/V4/SkusController.php +++ b/src/app/Http/Controllers/API/V4/SkusController.php @@ -54,7 +54,7 @@ public function index() { // Note: Order by title for consistent ordering in tests - $skus = Sku::withEnvTenant()->where('active', true)->orderBy('title')->get(); + $skus = Sku::withSubjectTenantContext()->where('active', true)->orderBy('title')->get(); $response = []; @@ -120,7 +120,7 @@ */ public function userSkus($id) { - $user = \App\User::withEnvTenant()->find($id); + $user = \App\User::withSubjectTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -134,7 +134,7 @@ $response = []; // Note: Order by title for consistent ordering in tests - $skus = Sku::withEnvTenant()->orderBy('title')->get(); + $skus = Sku::withObjectTenantContext($user)->orderBy('title')->get(); foreach ($skus as $sku) { if (!class_exists($sku->handler_class)) { diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -47,7 +47,7 @@ */ public function destroy($id) { - $user = User::find($id); + $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -95,7 +95,7 @@ */ public function show($id) { - $user = User::find($id); + $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -131,7 +131,7 @@ */ public function status($id) { - $user = User::find($id); + $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -216,7 +216,7 @@ } list ($local, $domain) = explode('@', $user->email); - $domain = Domain::where('namespace', $domain)->first(); + $domain = Domain::withEnvTenantContext()->where('namespace', $domain)->first(); // If that is not a public domain, add domain specific steps if ($domain && !$domain->isPublic()) { @@ -289,7 +289,7 @@ return $error_response; } - if (empty($request->package) || !($package = \App\Package::find($request->package))) { + if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) { $errors = ['package' => \trans('validation.packagerequired')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } @@ -340,7 +340,7 @@ */ public function update(Request $request, $id) { - $user = User::find($id); + $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); @@ -411,7 +411,7 @@ } // list of skus, [id=>obj] - $skus = Sku::all()->mapWithKeys( + $skus = Sku::withEnvTenantContext()->get()->mapWithKeys( function ($sku) { return [$sku->id => $sku]; } @@ -625,7 +625,7 @@ try { if (strpos($step, 'domain-') === 0) { list ($local, $domain) = explode('@', $user->email); - $domain = Domain::where('namespace', $domain)->first(); + $domain = Domain::withEnvTenantContext()->where('namespace', $domain)->first(); return DomainsController::execProcessStep($domain, $step); } @@ -689,7 +689,7 @@ } // Check if domain exists - $domain = Domain::where('namespace', $domain)->first(); + $domain = Domain::withEnvTenantContext()->where('namespace', $domain)->first(); if (empty($domain)) { return \trans('validation.domaininvalid'); @@ -763,7 +763,7 @@ } // Check if domain exists - $domain = Domain::where('namespace', $domain)->first(); + $domain = Domain::withEnvTenantContext()->where('namespace', $domain)->first(); if (empty($domain)) { return \trans('validation.domaininvalid'); diff --git a/src/app/Http/Middleware/AuthenticateReseller.php b/src/app/Http/Middleware/AuthenticateReseller.php --- a/src/app/Http/Middleware/AuthenticateReseller.php +++ b/src/app/Http/Middleware/AuthenticateReseller.php @@ -25,10 +25,6 @@ abort(403, "Unauthorized"); } - if ($user->tenant_id != \config('app.tenant_id')) { - abort(403, "Unauthorized"); - } - return $next($request); } } diff --git a/src/app/Observers/SkuObserver.php b/src/app/Observers/SkuObserver.php --- a/src/app/Observers/SkuObserver.php +++ b/src/app/Observers/SkuObserver.php @@ -24,7 +24,5 @@ } $sku->tenant_id = \config('app.tenant_id'); - - // TODO: We should make sure that tenant_id + title is unique } } diff --git a/src/app/Observers/UserAliasObserver.php b/src/app/Observers/UserAliasObserver.php --- a/src/app/Observers/UserAliasObserver.php +++ b/src/app/Observers/UserAliasObserver.php @@ -30,6 +30,13 @@ return false; } + if ($alias->user) { + if ($alias->user->tenant_id != $domain->tenant_id) { + \Log::error("Reseller for user '{$alias->user->email}' and domain '{$domain->namespace}' differ."); + return false; + } + } + return true; } diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php --- a/src/app/Providers/AppServiceProvider.php +++ b/src/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Query\Builder; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\DB; @@ -56,54 +57,94 @@ } // Register some template helpers - Blade::directive('theme_asset', function ($path) { - $path = trim($path, '/\'"'); - return ""; - }); + Blade::directive( + 'theme_asset', + function ($path) { + $path = trim($path, '/\'"'); + return ""; + } + ); - // Query builder 'withEnvTenant' macro - Builder::macro('withEnvTenant', function (string $table = null) { - $tenant_id = \config('app.tenant_id'); + Builder::macro( + 'withEnvTenantContext', + function (string $table = null) { + $tenantId = \config('app.tenant_id'); - if ($tenant_id) { - /** @var Builder $this */ - return $this->where(($table ? "$table." : '') . 'tenant_id', $tenant_id); + if ($tenantId) { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); + } + + return $this->whereNull(($table ? "$table." : "") . "tenant_id"); } + ); - /** @var Builder $this */ - return $this->whereNull(($table ? "$table." : '') . 'tenant_id'); - }); + Builder::macro( + 'withObjectTenantContext', + function (Model $object, string $table = null) { + // backend artisan cli + if (app()->runningInConsole()) { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $object->tenant_id); + } - // Query builder 'withUserTenant' macro - Builder::macro('withUserTenant', function (string $table = null) { - $tenant_id = auth()->user()->tenant_id; + $subject = auth()->user(); - if ($tenant_id) { - /** @var Builder $this */ - return $this->where(($table ? "$table." : '') . 'tenant_id', $tenant_id); - } + if ($subject->role == "admin") { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $object->tenant_id); + } - /** @var Builder $this */ - return $this->whereNull(($table ? "$table." : '') . 'tenant_id'); - }); + $tenantId = $subject->tenant_id; - // Query builder 'whereLike' mocro - Builder::macro('whereLike', function (string $column, string $search, int $mode = 0) { - $search = addcslashes($search, '%_'); - - switch ($mode) { - case 2: - $search .= '%'; - break; - case 1: - $search = '%' . $search; - break; - default: - $search = '%' . $search . '%'; + if ($tenantId) { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); + } + + return $this->whereNull(($table ? "$table." : "") . "tenant_id"); + } + ); + + Builder::macro( + 'withSubjectTenantContext', + function (string $table = null) { + // backend artisan cli + if (app()->runningInConsole()) { + $tenantId = \config('app.tenant_id'); + } else { + $tenantId = auth()->user()->tenant_id; + } + + if ($tenantId) { + /** @var Builder $this */ + return $this->where(($table ? "$table." : "") . "tenant_id", $tenantId); + } + + return $this->whereNull(($table ? "$table." : "") . "tenant_id"); } + ); + + // Query builder 'whereLike' mocro + Builder::macro( + 'whereLike', + function (string $column, string $search, int $mode = 0) { + $search = addcslashes($search, '%_'); + + switch ($mode) { + case 2: + $search .= '%'; + break; + case 1: + $search = '%' . $search; + break; + default: + $search = '%' . $search . '%'; + } - /** @var Builder $this */ - return $this->where($column, 'like', $search); - }); + /** @var Builder $this */ + return $this->where($column, 'like', $search); + } + ); } } diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php --- a/src/app/Providers/Payment/Mollie.php +++ b/src/app/Providers/Payment/Mollie.php @@ -584,13 +584,14 @@ ); $availableMethods = []; + foreach ($providerMethods as $method) { $availableMethods[$method->id] = [ 'id' => $method->id, 'name' => $method->description, 'minimumAmount' => round(floatval($method->minimumAmount->value) * 100), // Converted to cents 'currency' => $method->minimumAmount->currency, - 'exchangeRate' => $this->exchangeRate('CHF', $method->minimumAmount->currency) + 'exchangeRate' => \App\Utils::exchangeRate('CHF', $method->minimumAmount->currency) ]; } diff --git a/src/app/Tenant.php b/src/app/Tenant.php --- a/src/app/Tenant.php +++ b/src/app/Tenant.php @@ -13,6 +13,7 @@ class Tenant extends Model { protected $fillable = [ + 'id', 'title', ]; diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -312,7 +312,7 @@ if ($this->tenant_id) { $domains = Domain::where('tenant_id', $this->tenant_id); } else { - $domains = Domain::withEnvTenant(); + $domains = Domain::withEnvTenantContext(); } $domains = $domains->whereRaw(sprintf('(type & %s)', Domain::TYPE_PUBLIC)) diff --git a/src/app/Utils.php b/src/app/Utils.php --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -159,6 +159,38 @@ fclose($fp); } + + /** + * Generate a passphrase. Not intended for use in production, so limited to environments that are not production. + * + * @return string + */ + public static function generatePassphrase() + { + if (\config('app.env') == 'production') { + throw new \Exception("Thou shall not pass!"); + } + + if (\config('app.passphrase')) { + return \config('app.passphrase'); + } + + $alphaLow = 'abcdefghijklmnopqrstuvwxyz'; + $alphaUp = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $num = '0123456789'; + $stdSpecial = '~`!@#$%^&*()-_+=[{]}\\|\'";:/?.>,<'; + + $source = $alphaLow . $alphaUp . $num . $stdSpecial; + + $result = ''; + + for ($x = 0; $x < 16; $x++) { + $result .= substr($source, rand(0, (strlen($source) - 1)), 1); + } + + return $result; + } + /** * Calculate the broadcast address provided a net number and a prefix. * diff --git a/src/config/app.php b/src/config/app.php --- a/src/config/app.php +++ b/src/config/app.php @@ -53,6 +53,8 @@ 'url' => env('APP_URL', 'http://localhost'), + 'passphrase' => env('APP_PASSPHRASE', null), + 'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')), 'asset_url' => env('ASSET_URL', null), diff --git a/src/database/migrations/2020_05_05_095212_create_tenants_table.php b/src/database/migrations/2020_05_05_095212_create_tenants_table.php --- a/src/database/migrations/2020_05_05_095212_create_tenants_table.php +++ b/src/database/migrations/2020_05_05_095212_create_tenants_table.php @@ -23,19 +23,21 @@ } ); - \App\Tenant::create(['title' => 'Kolab Now']); + $tenantId = \config('app.tenant_id'); - foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $table_name) { + $tenant = \App\Tenant::create(['id' => $tenantId, 'title' => 'Kolab Now']); + + foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $tableName) { Schema::table( - $table_name, + $tableName, function (Blueprint $table) { $table->bigInteger('tenant_id')->unsigned()->nullable(); $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null'); } ); - if ($tenant_id = \config('app.tenant_id')) { - DB::statement("UPDATE `{$table_name}` SET `tenant_id` = {$tenant_id}"); + if ($tenantId) { + DB::statement("UPDATE `{$tableName}` SET `tenant_id` = {$tenantId}"); } } @@ -48,9 +50,6 @@ } ); } - - // FIXME: Should we also have package_skus.fee ? - // We have package_skus.cost, but I think it is not used anywhere. } /** @@ -60,9 +59,9 @@ */ public function down() { - foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $table_name) { + foreach (['users', 'discounts', 'domains', 'plans', 'packages', 'skus'] as $tableName) { Schema::table( - $table_name, + $tableName, function (Blueprint $table) { $table->dropForeign(['tenant_id']); $table->dropColumn('tenant_id'); diff --git a/src/database/seeds/local/DiscountSeeder.php b/src/database/seeds/local/DiscountSeeder.php --- a/src/database/seeds/local/DiscountSeeder.php +++ b/src/database/seeds/local/DiscountSeeder.php @@ -38,5 +38,21 @@ 'code' => 'TEST', ] ); + + // We're running in reseller mode, add a sample discount + $tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get(); + + foreach ($tenants as $tenant) { + $discount = Discount::create( + [ + 'description' => "Sample Discount by Reseller '{$tenant->title}'", + 'discount' => 10, + 'active' => true, + ] + ); + + $discount->tenant_id = $tenant->id; + $discount->save(); + } } } diff --git a/src/database/seeds/local/DomainSeeder.php b/src/database/seeds/local/DomainSeeder.php --- a/src/database/seeds/local/DomainSeeder.php +++ b/src/database/seeds/local/DomainSeeder.php @@ -33,7 +33,7 @@ [ 'namespace' => $domain, 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_PUBLIC + 'type' => Domain::TYPE_PUBLIC, ] ); } @@ -43,7 +43,7 @@ [ 'namespace' => \config('app.domain'), 'status' => DOMAIN::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_PUBLIC + 'type' => Domain::TYPE_PUBLIC, ] ); } @@ -59,23 +59,27 @@ [ 'namespace' => $domain, 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_EXTERNAL + 'type' => Domain::TYPE_EXTERNAL, ] ); } - // example tenant domain, note that 'tenant_id' is not a fillable. - $domain = Domain::create( - [ - 'namespace' => 'example-tenant.dev-local', - 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, - 'type' => Domain::TYPE_PUBLIC - ] - ); + // We're running in reseller mode, add a sample discount + $tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get(); - $tenant = \App\Tenant::where('title', 'Sample Tenant')->first(); + foreach ($tenants as $tenant) { + $domainNamespace = strtolower(str_replace(' ', '-', $tenant->title)) . '.dev-local'; - $domain->tenant_id = $tenant->id; - $domain->save(); + $domain = Domain::create( + [ + 'namespace' => $domainNamespace, + 'status' => Domain::STATUS_CONFIRMED + Domain::STATUS_ACTIVE, + 'type' => Domain::TYPE_PUBLIC, + ] + ); + + $domain->tenant_id = $tenant->id; + $domain->save(); + } } } diff --git a/src/database/seeds/local/PackageSeeder.php b/src/database/seeds/local/PackageSeeder.php --- a/src/database/seeds/local/PackageSeeder.php +++ b/src/database/seeds/local/PackageSeeder.php @@ -15,16 +15,17 @@ */ public function run() { - $skuGroupware = Sku::firstOrCreate(['title' => 'groupware']); - $skuMailbox = Sku::firstOrCreate(['title' => 'mailbox']); - $skuStorage = Sku::firstOrCreate(['title' => 'storage']); + $skuDomain = Sku::where(['title' => 'domain-hosting', 'tenant_id' => \config('app.tenant_id')])->first(); + $skuGroupware = Sku::where(['title' => 'groupware', 'tenant_id' => \config('app.tenant_id')])->first(); + $skuMailbox = Sku::where(['title' => 'mailbox', 'tenant_id' => \config('app.tenant_id')])->first(); + $skuStorage = Sku::where(['title' => 'storage', 'tenant_id' => \config('app.tenant_id')])->first(); $package = Package::create( [ 'title' => 'kolab', 'name' => 'Groupware Account', 'description' => 'A fully functional groupware account.', - 'discount_rate' => 0 + 'discount_rate' => 0, ] ); @@ -40,7 +41,7 @@ // be the number of SKU free units. $package->skus()->updateExistingPivot( $skuStorage, - ['qty' => 2], + ['qty' => 5], false ); @@ -49,7 +50,7 @@ 'title' => 'lite', 'name' => 'Lite Account', 'description' => 'Just mail and no more.', - 'discount_rate' => 0 + 'discount_rate' => 0, ] ); @@ -61,8 +62,8 @@ $package->skus()->saveMany($skus); $package->skus()->updateExistingPivot( - Sku::firstOrCreate(['title' => 'storage']), - ['qty' => 2], + $skuStorage, + ['qty' => 5], false ); @@ -71,14 +72,95 @@ 'title' => 'domain-hosting', 'name' => 'Domain Hosting', 'description' => 'Use your own, existing domain.', - 'discount_rate' => 0 + 'discount_rate' => 0, ] ); $skus = [ - Sku::firstOrCreate(['title' => 'domain-hosting']) + $skuDomain ]; $package->skus()->saveMany($skus); + + // We're running in reseller mode, add a sample discount + $tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get(); + + foreach ($tenants as $tenant) { + $skuDomain = Sku::where(['title' => 'domain-hosting', 'tenant_id' => $tenant->id])->first(); + $skuGroupware = Sku::where(['title' => 'groupware', 'tenant_id' => $tenant->id])->first(); + $skuMailbox = Sku::where(['title' => 'mailbox', 'tenant_id' => $tenant->id])->first(); + $skuStorage = Sku::where(['title' => 'storage', 'tenant_id' => $tenant->id])->first(); + + $package = Package::create( + [ + 'title' => 'kolab', + 'name' => 'Groupware Account', + 'description' => 'A fully functional groupware account.', + 'discount_rate' => 0 + ] + ); + + $package->tenant_id = $tenant->id; + $package->save(); + + $skus = [ + $skuMailbox, + $skuGroupware, + $skuStorage + ]; + + $package->skus()->saveMany($skus); + + // This package contains 2 units of the storage SKU, which just so happens to also + // be the number of SKU free units. + $package->skus()->updateExistingPivot( + $skuStorage, + ['qty' => 5], + false + ); + + $package = Package::create( + [ + 'title' => 'lite', + 'name' => 'Lite Account', + 'description' => 'Just mail and no more.', + 'discount_rate' => 0 + ] + ); + + $package->tenant_id = $tenant->id; + $package->save(); + + $skus = [ + $skuMailbox, + $skuStorage + ]; + + $package->skus()->saveMany($skus); + + $package->skus()->updateExistingPivot( + $skuStorage, + ['qty' => 5], + false + ); + + $package = Package::create( + [ + 'title' => 'domain-hosting', + 'name' => 'Domain Hosting', + 'description' => 'Use your own, existing domain.', + 'discount_rate' => 0 + ] + ); + + $package->tenant_id = $tenant->id; + $package->save(); + + $skus = [ + $skuDomain + ]; + + $package->skus()->saveMany($skus); + } } } diff --git a/src/database/seeds/local/PlanSeeder.php b/src/database/seeds/local/PlanSeeder.php --- a/src/database/seeds/local/PlanSeeder.php +++ b/src/database/seeds/local/PlanSeeder.php @@ -15,101 +15,6 @@ */ public function run() { - /* - $plan = Plan::create( - [ - 'title' => 'family', - 'description' => 'A group of accounts for 2 or more users.', - 'discount_qty' => 0, - 'discount_rate' => 0 - ] - ); - - $packages = [ - Package::firstOrCreate(['title' => 'kolab']), - Package::firstOrCreate(['title' => 'domain-hosting']) - ]; - - $plan->packages()->saveMany($packages); - - $plan->packages()->updateExistingPivot( - Package::firstOrCreate(['title' => 'kolab']), - [ - 'qty_min' => 2, - 'qty_max' => -1, - 'discount_qty' => 2, - 'discount_rate' => 50 - ], - false - ); - - $plan = Plan::create( - [ - 'title' => 'small-business', - 'description' => 'Accounts for small business owners.', - 'discount_qty' => 0, - 'discount_rate' => 10 - ] - ); - - $packages = [ - Package::firstOrCreate(['title' => 'kolab']), - Package::firstOrCreate(['title' => 'domain-hosting']) - ]; - - $plan->packages()->saveMany($packages); - - $plan->packages()->updateExistingPivot( - Package::firstOrCreate(['title' => 'kolab']), - [ - 'qty_min' => 5, - 'qty_max' => 25, - 'discount_qty' => 5, - 'discount_rate' => 30 - ], - false - ); - - $plan = Plan::create( - [ - 'title' => 'large-business', - 'description' => 'Accounts for large businesses.', - 'discount_qty' => 0, - 'discount_rate' => 10 - ] - ); - - $packages = [ - Package::firstOrCreate(['title' => 'kolab']), - Package::firstOrCreate(['title' => 'lite']), - Package::firstOrCreate(['title' => 'domain-hosting']) - ]; - - $plan->packages()->saveMany($packages); - - $plan->packages()->updateExistingPivot( - Package::firstOrCreate(['title' => 'kolab']), - [ - 'qty_min' => 20, - 'qty_max' => -1, - 'discount_qty' => 10, - 'discount_rate' => 10 - ], - false - ); - - $plan->packages()->updateExistingPivot( - Package::firstOrCreate(['title' => 'lite']), - [ - 'qty_min' => 0, - 'qty_max' => -1, - 'discount_qty' => 10, - 'discount_rate' => 10 - ], - false - ); - */ - $description = <<<'EOD'

Everything you need to get started or try Kolab Now, including: